@cubicj/codex-watch 0.1.0 → 0.2.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.
- package/README.md +2 -1
- package/bin/codex-watch +156 -36
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Live-tail the newest Codex plugin background job log for the current git repo.
|
|
|
9
9
|
- Resolves the git repo from the terminal's current working directory.
|
|
10
10
|
- Follows the newest job log written by the `openai/codex-plugin-cc` Claude Code plugin.
|
|
11
11
|
- Auto-switches when a `--resume` starts a newer job id, so a resumed run is picked up without restarting.
|
|
12
|
+
- Shows the job's model, reasoning effort, sandbox mode, and write access under the header (resolved from the Codex CLI session files in `$CODEX_HOME`, default `~/.codex`).
|
|
12
13
|
- Colorizes assistant messages, command status, file changes, and job events.
|
|
13
14
|
- Prints a completion banner such as `✓ completed · <duration>` or `✗ <status> · <duration>`.
|
|
14
15
|
- Drops duplicate captured lines.
|
|
@@ -32,7 +33,7 @@ codex-watch
|
|
|
32
33
|
From source:
|
|
33
34
|
|
|
34
35
|
```sh
|
|
35
|
-
git clone
|
|
36
|
+
git clone https://github.com/cubicj/codex-watch.git
|
|
36
37
|
cd codex-watch
|
|
37
38
|
npm link
|
|
38
39
|
codex-watch
|
package/bin/codex-watch
CHANGED
|
@@ -3,6 +3,14 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const childProcess = require("child_process");
|
|
6
|
+
const { StringDecoder } = require("node:string_decoder");
|
|
7
|
+
|
|
8
|
+
process.stdout.on("error", (error) => {
|
|
9
|
+
if (error.code === "EPIPE") {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
throw error;
|
|
13
|
+
});
|
|
6
14
|
|
|
7
15
|
const args = process.argv.slice(2);
|
|
8
16
|
const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
@@ -53,17 +61,21 @@ const slug = makeSlug(repoRoot);
|
|
|
53
61
|
const stateRoot = process.env.CLAUDE_PLUGIN_DATA
|
|
54
62
|
? path.join(process.env.CLAUDE_PLUGIN_DATA, "state")
|
|
55
63
|
: path.join(os.homedir(), ".claude/plugins/data/codex-openai-codex/state");
|
|
64
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
56
65
|
const stateDirPattern = new RegExp("^" + escapeRegExp(slug) + "-[0-9a-f]{6,}$");
|
|
57
66
|
|
|
58
67
|
let currentJob = null;
|
|
59
|
-
let currentKey = "";
|
|
60
68
|
let logOffset = 0;
|
|
69
|
+
let logDecoder = new StringDecoder("utf8");
|
|
61
70
|
let pendingText = "";
|
|
62
71
|
let assistantBlock = false;
|
|
63
72
|
let tailTimer = null;
|
|
64
73
|
let scanTimer = null;
|
|
65
74
|
let waitingPrinted = false;
|
|
66
75
|
let completionBannerPrinted = false;
|
|
76
|
+
let sessionInfoPrinted = false;
|
|
77
|
+
let sessionRolloutPath = "";
|
|
78
|
+
let lastRolloutSearchAt = null;
|
|
67
79
|
|
|
68
80
|
process.on("SIGINT", () => {
|
|
69
81
|
clearTimers();
|
|
@@ -78,6 +90,7 @@ if (once) {
|
|
|
78
90
|
process.exit(0);
|
|
79
91
|
}
|
|
80
92
|
printHeader(job);
|
|
93
|
+
printSessionInfo(job);
|
|
81
94
|
renderWholeLog(job);
|
|
82
95
|
process.exit(0);
|
|
83
96
|
}
|
|
@@ -160,10 +173,13 @@ function findNewestJob() {
|
|
|
160
173
|
jsonPath,
|
|
161
174
|
logPath,
|
|
162
175
|
createdAt,
|
|
163
|
-
displayedCreatedAt: typeof meta.createdAt === "string" ? meta.createdAt : createdAt,
|
|
176
|
+
displayedCreatedAt: typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : createdAt,
|
|
177
|
+
startedAt: typeof meta.startedAt === "string" ? meta.startedAt : "",
|
|
164
178
|
status: typeof meta.status === "string" ? meta.status : "unknown",
|
|
165
179
|
phase: typeof meta.phase === "string" ? meta.phase : "unknown",
|
|
166
|
-
summary: typeof meta.summary === "string" ? meta.summary : ""
|
|
180
|
+
summary: typeof meta.summary === "string" ? meta.summary : "",
|
|
181
|
+
threadId: typeof meta.threadId === "string" ? meta.threadId : "",
|
|
182
|
+
write: typeof meta.write === "boolean" ? meta.write : false
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
185
|
}
|
|
@@ -194,28 +210,32 @@ function scanForNewJob() {
|
|
|
194
210
|
}
|
|
195
211
|
|
|
196
212
|
if (!currentJob) {
|
|
197
|
-
switchToJob(newest
|
|
213
|
+
switchToJob(newest);
|
|
198
214
|
return;
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
if (newest.createdAt > currentJob.createdAt) {
|
|
202
218
|
console.log(color("dim", "──────── switched to " + newest.id + " ────────"));
|
|
203
|
-
switchToJob(newest
|
|
219
|
+
switchToJob(newest);
|
|
204
220
|
}
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
function switchToJob(job) {
|
|
208
224
|
currentJob = job;
|
|
209
|
-
currentKey = job.jsonPath;
|
|
210
225
|
logOffset = 0;
|
|
226
|
+
logDecoder = new StringDecoder("utf8");
|
|
211
227
|
pendingText = "";
|
|
212
228
|
assistantBlock = false;
|
|
213
229
|
waitingPrinted = false;
|
|
214
230
|
completionBannerPrinted = false;
|
|
231
|
+
sessionInfoPrinted = false;
|
|
232
|
+
sessionRolloutPath = "";
|
|
233
|
+
lastRolloutSearchAt = null;
|
|
215
234
|
if (tailTimer) {
|
|
216
235
|
clearInterval(tailTimer);
|
|
217
236
|
}
|
|
218
237
|
printHeader(job);
|
|
238
|
+
printSessionInfo(job);
|
|
219
239
|
readAvailableLog();
|
|
220
240
|
tailTimer = setInterval(readAvailableLog, 400);
|
|
221
241
|
}
|
|
@@ -235,6 +255,100 @@ function printHeader(job) {
|
|
|
235
255
|
console.log(color("dim", line));
|
|
236
256
|
}
|
|
237
257
|
|
|
258
|
+
function printSessionInfo(job) {
|
|
259
|
+
if (sessionInfoPrinted || !job.threadId) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!sessionRolloutPath) {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
if (lastRolloutSearchAt !== null && now - lastRolloutSearchAt < 5000) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
lastRolloutSearchAt = now;
|
|
269
|
+
sessionRolloutPath = findRolloutFile(job.threadId);
|
|
270
|
+
if (!sessionRolloutPath) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const text = readFileText(sessionRolloutPath);
|
|
276
|
+
const startedAt = typeof job.startedAt === "string" && job.startedAt
|
|
277
|
+
? job.startedAt
|
|
278
|
+
: typeof job.createdAt === "string"
|
|
279
|
+
? job.createdAt
|
|
280
|
+
: "";
|
|
281
|
+
let turnContext = null;
|
|
282
|
+
|
|
283
|
+
for (const line of text.split(/\r?\n/)) {
|
|
284
|
+
try {
|
|
285
|
+
const entry = JSON.parse(line);
|
|
286
|
+
if (
|
|
287
|
+
entry &&
|
|
288
|
+
entry.type === "turn_context" &&
|
|
289
|
+
(!startedAt || (typeof entry.timestamp === "string" && entry.timestamp >= startedAt))
|
|
290
|
+
) {
|
|
291
|
+
turnContext = entry;
|
|
292
|
+
}
|
|
293
|
+
} catch (_error) {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const payload = turnContext && turnContext.payload;
|
|
297
|
+
if (!payload || typeof payload.model !== "string" || !payload.model) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const segments = ["model " + payload.model];
|
|
302
|
+
if (typeof payload.effort === "string" && payload.effort) {
|
|
303
|
+
segments.push("effort " + payload.effort);
|
|
304
|
+
}
|
|
305
|
+
if (payload.sandbox_policy && typeof payload.sandbox_policy.type === "string" && payload.sandbox_policy.type) {
|
|
306
|
+
segments.push("sandbox " + payload.sandbox_policy.type);
|
|
307
|
+
}
|
|
308
|
+
segments.push(job.write === true ? "write" : "read-only");
|
|
309
|
+
console.log(color("dim", segments.join(" · ")));
|
|
310
|
+
sessionInfoPrinted = true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function findRolloutFile(threadId) {
|
|
314
|
+
const sessionsRoot = path.join(codexHome, "sessions");
|
|
315
|
+
const years = safeReadDir(sessionsRoot, { withFileTypes: true })
|
|
316
|
+
.filter((entry) => entry.isDirectory())
|
|
317
|
+
.map((entry) => entry.name)
|
|
318
|
+
.sort()
|
|
319
|
+
.reverse();
|
|
320
|
+
|
|
321
|
+
for (const year of years) {
|
|
322
|
+
const yearDir = path.join(sessionsRoot, year);
|
|
323
|
+
const months = safeReadDir(yearDir, { withFileTypes: true })
|
|
324
|
+
.filter((entry) => entry.isDirectory())
|
|
325
|
+
.map((entry) => entry.name)
|
|
326
|
+
.sort()
|
|
327
|
+
.reverse();
|
|
328
|
+
|
|
329
|
+
for (const month of months) {
|
|
330
|
+
const monthDir = path.join(yearDir, month);
|
|
331
|
+
const days = safeReadDir(monthDir, { withFileTypes: true })
|
|
332
|
+
.filter((entry) => entry.isDirectory())
|
|
333
|
+
.map((entry) => entry.name)
|
|
334
|
+
.sort()
|
|
335
|
+
.reverse();
|
|
336
|
+
|
|
337
|
+
for (const day of days) {
|
|
338
|
+
const dayDir = path.join(monthDir, day);
|
|
339
|
+
const rollout = safeReadDir(dayDir, { withFileTypes: true }).find(
|
|
340
|
+
(entry) => entry.isFile() && entry.name.endsWith("-" + threadId + ".jsonl")
|
|
341
|
+
);
|
|
342
|
+
if (rollout) {
|
|
343
|
+
return path.join(dayDir, rollout.name);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
|
|
238
352
|
function formatElapsed(createdAt, updatedAt) {
|
|
239
353
|
const started = Date.parse(createdAt);
|
|
240
354
|
if (!Number.isFinite(started)) {
|
|
@@ -261,35 +375,36 @@ function renderWholeLog(job) {
|
|
|
261
375
|
}
|
|
262
376
|
|
|
263
377
|
function readAvailableLog() {
|
|
264
|
-
if (!currentJob
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!checkJobCompletion()) {
|
|
378
|
+
if (!currentJob) {
|
|
269
379
|
return;
|
|
270
380
|
}
|
|
271
381
|
|
|
272
382
|
const stat = safeStat(currentJob.logPath);
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
assistantBlock = false;
|
|
281
|
-
}
|
|
383
|
+
if (stat && stat.isFile()) {
|
|
384
|
+
if (stat.size < logOffset) {
|
|
385
|
+
logOffset = 0;
|
|
386
|
+
logDecoder = new StringDecoder("utf8");
|
|
387
|
+
pendingText = "";
|
|
388
|
+
assistantBlock = false;
|
|
389
|
+
}
|
|
282
390
|
|
|
283
|
-
|
|
284
|
-
|
|
391
|
+
if (stat.size !== logOffset) {
|
|
392
|
+
const chunk = readFileChunk(currentJob.logPath, logOffset, stat.size - logOffset);
|
|
393
|
+
if (chunk !== null) {
|
|
394
|
+
logOffset += chunk.length;
|
|
395
|
+
renderText(logDecoder.write(chunk), true);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
285
398
|
}
|
|
286
399
|
|
|
287
|
-
|
|
288
|
-
logOffset = stat.size;
|
|
289
|
-
renderText(chunk, true);
|
|
400
|
+
checkJobCompletion();
|
|
290
401
|
}
|
|
291
402
|
|
|
292
403
|
function checkJobCompletion() {
|
|
404
|
+
if (completionBannerPrinted) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
293
408
|
let meta = null;
|
|
294
409
|
try {
|
|
295
410
|
meta = JSON.parse(fs.readFileSync(currentJob.jsonPath, "utf8"));
|
|
@@ -298,13 +413,19 @@ function checkJobCompletion() {
|
|
|
298
413
|
}
|
|
299
414
|
|
|
300
415
|
const status = typeof meta.status === "string" ? meta.status : "unknown";
|
|
301
|
-
|
|
416
|
+
currentJob.threadId = typeof meta.threadId === "string" ? meta.threadId : "";
|
|
417
|
+
currentJob.write = typeof meta.write === "boolean" ? meta.write : false;
|
|
418
|
+
currentJob.startedAt = typeof meta.startedAt === "string" ? meta.startedAt : "";
|
|
419
|
+
printSessionInfo(currentJob);
|
|
420
|
+
if (!isTerminalStatus(status)) {
|
|
302
421
|
return true;
|
|
303
422
|
}
|
|
304
423
|
|
|
305
424
|
const createdAt = typeof meta.createdAt === "string" && meta.createdAt ? meta.createdAt : currentJob.displayedCreatedAt;
|
|
425
|
+
const startedAt = typeof meta.startedAt === "string" && meta.startedAt ? meta.startedAt : createdAt;
|
|
306
426
|
const updatedAt = typeof meta.updatedAt === "string" && meta.updatedAt ? meta.updatedAt : "";
|
|
307
|
-
const
|
|
427
|
+
const completedAt = typeof meta.completedAt === "string" && meta.completedAt ? meta.completedAt : updatedAt;
|
|
428
|
+
const elapsed = formatElapsed(startedAt, completedAt);
|
|
308
429
|
if (status === "completed") {
|
|
309
430
|
console.log(color("green", "✓ completed · " + elapsed));
|
|
310
431
|
} else {
|
|
@@ -315,7 +436,7 @@ function checkJobCompletion() {
|
|
|
315
436
|
}
|
|
316
437
|
|
|
317
438
|
function isTerminalStatus(status) {
|
|
318
|
-
return status !== "running" && status !== "queued"
|
|
439
|
+
return status !== "running" && status !== "queued";
|
|
319
440
|
}
|
|
320
441
|
|
|
321
442
|
function readFileText(file) {
|
|
@@ -332,16 +453,14 @@ function readFileChunk(file, start, length) {
|
|
|
332
453
|
fd = fs.openSync(file, "r");
|
|
333
454
|
const buffer = Buffer.alloc(length);
|
|
334
455
|
const bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
|
335
|
-
return buffer.subarray(0, bytesRead)
|
|
456
|
+
return buffer.subarray(0, bytesRead);
|
|
336
457
|
} catch (_error) {
|
|
337
|
-
return
|
|
458
|
+
return null;
|
|
338
459
|
} finally {
|
|
339
460
|
if (fd !== null) {
|
|
340
461
|
try {
|
|
341
462
|
fs.closeSync(fd);
|
|
342
|
-
} catch (_error) {
|
|
343
|
-
return "";
|
|
344
|
-
}
|
|
463
|
+
} catch (_error) {}
|
|
345
464
|
}
|
|
346
465
|
}
|
|
347
466
|
}
|
|
@@ -368,7 +487,7 @@ function renderText(text, keepPartial) {
|
|
|
368
487
|
}
|
|
369
488
|
|
|
370
489
|
function renderLine(line) {
|
|
371
|
-
const match = line.match(/^\[[^\]]
|
|
490
|
+
const match = line.match(/^\[\d{4}-\d{2}-\d{2}T[^\]]*\]\s+(.*)$/);
|
|
372
491
|
if (!match) {
|
|
373
492
|
if (assistantBlock) {
|
|
374
493
|
console.log(assistantText(line));
|
|
@@ -427,8 +546,9 @@ function renderLine(line) {
|
|
|
427
546
|
|
|
428
547
|
function truncate(value) {
|
|
429
548
|
const maxLength = 140;
|
|
430
|
-
|
|
549
|
+
const codePoints = Array.from(value);
|
|
550
|
+
if (codePoints.length <= maxLength) {
|
|
431
551
|
return value;
|
|
432
552
|
}
|
|
433
|
-
return
|
|
553
|
+
return codePoints.slice(0, maxLength - 1).join("") + "…";
|
|
434
554
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicj/codex-watch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Live-tail the newest Codex plugin background job log for the current git repo.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/cubicj/codex-watch.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/cubicj/codex-watch/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/cubicj/codex-watch#readme",
|
|
6
14
|
"publishConfig": {
|
|
7
15
|
"access": "public"
|
|
8
16
|
},
|