parawatch 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/adapters/claudeCode.js +331 -0
- package/dist/adapters/codex.js +185 -0
- package/dist/adapters/index.js +18 -0
- package/dist/adapters/jsonl.js +144 -0
- package/dist/bin/cli.js +491 -0
- package/dist/core/desktop.js +102 -0
- package/dist/core/digest.js +146 -0
- package/dist/core/follow.js +70 -0
- package/dist/core/format.js +351 -0
- package/dist/core/github.js +442 -0
- package/dist/core/integrate.js +121 -0
- package/dist/core/ops.js +415 -0
- package/dist/core/runs.js +284 -0
- package/dist/core/skill.js +67 -0
- package/dist/core/snapshot.js +71 -0
- package/dist/core/state.js +107 -0
- package/dist/core/store.js +127 -0
- package/dist/core/types.js +6 -0
- package/package.json +57 -4
- package/skills/parawatch/SKILL.md +135 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { applyStall } from "./claudeCode.js";
|
|
5
|
+
import { isObj, logActivity, oneLine, readHeadLines, readHeadText, readTailLines, str, textValues } from "./jsonl.js";
|
|
6
|
+
const HEAD_BYTES = 128 * 1024;
|
|
7
|
+
const TAIL_BYTES = 256 * 1024;
|
|
8
|
+
// prompts can sit behind several inline base64 images on one line; read this far only when needed
|
|
9
|
+
const RAW_HEAD_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
export function codexSessionsDir() {
|
|
11
|
+
if (process.env.PARAWATCH_CODEX_DIR)
|
|
12
|
+
return process.env.PARAWATCH_CODEX_DIR;
|
|
13
|
+
const base = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
14
|
+
return path.join(base, "sessions");
|
|
15
|
+
}
|
|
16
|
+
const PARENT_IN_CWD = /\/claude-\d+\/[^/]+\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\/|$)/;
|
|
17
|
+
/** A Claude Code session id when Codex was started from inside that session's scratch space. */
|
|
18
|
+
export function parentFromCwd(cwd) {
|
|
19
|
+
return cwd ? PARENT_IN_CWD.exec(cwd)?.[1] : undefined;
|
|
20
|
+
}
|
|
21
|
+
function payload(r) {
|
|
22
|
+
return isObj(r) && isObj(r.payload) ? r.payload : undefined;
|
|
23
|
+
}
|
|
24
|
+
function messageText(p) {
|
|
25
|
+
const c = p.content;
|
|
26
|
+
if (!Array.isArray(c))
|
|
27
|
+
return "";
|
|
28
|
+
return c
|
|
29
|
+
.filter(isObj)
|
|
30
|
+
.map((b) => str(b.text) ?? "")
|
|
31
|
+
.join("\n")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
function promptBlock(p) {
|
|
35
|
+
const c = p.content;
|
|
36
|
+
if (!Array.isArray(c))
|
|
37
|
+
return undefined;
|
|
38
|
+
for (const b of c.filter(isObj)) {
|
|
39
|
+
const t = (str(b.text) ?? "").trim();
|
|
40
|
+
if (!t || t.startsWith("<") || t.startsWith("# AGENTS.md instructions"))
|
|
41
|
+
continue;
|
|
42
|
+
return t;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function toolSummary(p) {
|
|
47
|
+
const name = str(p.name) ?? "tool";
|
|
48
|
+
let arg = str(p.input) ?? str(p.arguments) ?? "";
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(arg);
|
|
51
|
+
if (isObj(parsed))
|
|
52
|
+
arg = str(parsed.cmd) ?? str(parsed.command) ?? (Array.isArray(parsed.command) ? parsed.command.join(" ") : arg);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// free-form input
|
|
56
|
+
}
|
|
57
|
+
return arg ? `${name}: ${oneLine(arg, 80)}` : name;
|
|
58
|
+
}
|
|
59
|
+
/** The first real prompt in user messages whose lines were too long to parse (inline images). */
|
|
60
|
+
export function promptFromRaw(raw) {
|
|
61
|
+
for (const line of raw.split("\n")) {
|
|
62
|
+
if (!line.includes('"role":"user"'))
|
|
63
|
+
continue;
|
|
64
|
+
for (const t of textValues(line)) {
|
|
65
|
+
const v = t.trim();
|
|
66
|
+
if (v && !v.startsWith("<") && !v.startsWith("# AGENTS.md instructions"))
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
export function readCodexLog(head, tail, rawHead) {
|
|
73
|
+
const meta = head.map(payload).find((p, i) => p && isObj(head[i]) && head[i].type === "session_meta");
|
|
74
|
+
const id = str(meta?.id) ?? str(meta?.session_id) ?? "unknown";
|
|
75
|
+
const cwd = str(meta?.cwd);
|
|
76
|
+
const exec = str(meta?.originator) === "codex_exec" || str(meta?.source) === "exec";
|
|
77
|
+
let prompt;
|
|
78
|
+
for (const r of head) {
|
|
79
|
+
const p = payload(r);
|
|
80
|
+
if (isObj(r) && r.type === "response_item" && p?.type === "message" && p.role === "user") {
|
|
81
|
+
// skip injected context: environment blocks, image tags, AGENTS.md instructions
|
|
82
|
+
const t = promptBlock(p);
|
|
83
|
+
if (t) {
|
|
84
|
+
prompt = t;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!prompt && rawHead)
|
|
90
|
+
prompt = promptFromRaw(rawHead());
|
|
91
|
+
const run = {
|
|
92
|
+
id,
|
|
93
|
+
runtime: "codex",
|
|
94
|
+
kind: exec ? "exec" : "session",
|
|
95
|
+
title: prompt ? oneLine(prompt, 60) : id.slice(0, 8),
|
|
96
|
+
cwd,
|
|
97
|
+
parent: parentFromCwd(cwd),
|
|
98
|
+
startedAt: str(meta?.timestamp) ?? (isObj(head[0]) ? str(head[0].timestamp) : undefined),
|
|
99
|
+
state: "working",
|
|
100
|
+
prompt: prompt ? oneLine(prompt, 400) : undefined,
|
|
101
|
+
};
|
|
102
|
+
let lastStart = -1;
|
|
103
|
+
let lastDone = -1;
|
|
104
|
+
let lastError = -1;
|
|
105
|
+
tail.forEach((r, i) => {
|
|
106
|
+
if (!isObj(r))
|
|
107
|
+
return;
|
|
108
|
+
const ts = str(r.timestamp);
|
|
109
|
+
if (ts && (!run.lastActivity || ts > run.lastActivity))
|
|
110
|
+
run.lastActivity = ts;
|
|
111
|
+
const p = payload(r);
|
|
112
|
+
if (!p)
|
|
113
|
+
return;
|
|
114
|
+
if (r.type === "event_msg" && p.type === "task_started")
|
|
115
|
+
lastStart = i;
|
|
116
|
+
if (r.type === "event_msg" && p.type === "task_complete") {
|
|
117
|
+
lastDone = i;
|
|
118
|
+
const msg = str(p.last_agent_message);
|
|
119
|
+
if (msg)
|
|
120
|
+
run.action = oneLine(msg, 100);
|
|
121
|
+
}
|
|
122
|
+
if (r.type === "event_msg" && (p.type === "error" || p.type === "turn_aborted"))
|
|
123
|
+
lastError = i;
|
|
124
|
+
if (r.type === "response_item" && (p.type === "custom_tool_call" || p.type === "function_call")) {
|
|
125
|
+
if (i > lastDone)
|
|
126
|
+
run.action = toolSummary(p);
|
|
127
|
+
}
|
|
128
|
+
if (r.type === "response_item" && p.type === "message" && p.role === "assistant" && i > lastDone) {
|
|
129
|
+
const t = messageText(p);
|
|
130
|
+
if (t)
|
|
131
|
+
run.action = oneLine(t, 100);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (lastError > Math.max(lastStart, lastDone))
|
|
135
|
+
run.state = "failed";
|
|
136
|
+
else if (lastDone > lastStart)
|
|
137
|
+
run.state = exec ? "done" : "idle";
|
|
138
|
+
return run;
|
|
139
|
+
}
|
|
140
|
+
function dayDirs(root, sinceMs, now) {
|
|
141
|
+
const out = [];
|
|
142
|
+
const day = 86_400_000;
|
|
143
|
+
for (let t = sinceMs - day; t <= now + day; t += day) {
|
|
144
|
+
const d = new Date(t);
|
|
145
|
+
const y = String(d.getFullYear());
|
|
146
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
147
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
148
|
+
const dir = path.join(root, y, m, dd);
|
|
149
|
+
if (!out.includes(dir))
|
|
150
|
+
out.push(dir);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
export const codex = {
|
|
155
|
+
name: "codex",
|
|
156
|
+
scan(opts) {
|
|
157
|
+
const root = codexSessionsDir();
|
|
158
|
+
const runs = [];
|
|
159
|
+
for (const dir of dayDirs(root, opts.sinceMs, opts.now)) {
|
|
160
|
+
let files;
|
|
161
|
+
try {
|
|
162
|
+
files = fs.readdirSync(dir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl"));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
const file = path.join(dir, f);
|
|
169
|
+
try {
|
|
170
|
+
if (fs.statSync(file).mtimeMs < opts.sinceMs)
|
|
171
|
+
continue;
|
|
172
|
+
const run = { ...readCodexLog(readHeadLines(file, HEAD_BYTES), readTailLines(file, TAIL_BYTES), () => readHeadText(file, RAW_HEAD_BYTES)), source: file };
|
|
173
|
+
run.activity = logActivity(file, opts.now);
|
|
174
|
+
applyStall(run, undefined, opts.now);
|
|
175
|
+
runs.push(run);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
runs.push({ id: f, runtime: "codex", kind: "exec", title: f, state: "unknown", source: file, error: `could not read: ${err.message}` });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return runs;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { claudeCode } from "./claudeCode.js";
|
|
2
|
+
import { codex } from "./codex.js";
|
|
3
|
+
export const adapters = [claudeCode, codex];
|
|
4
|
+
export function scanAll(opts, only) {
|
|
5
|
+
const runs = [];
|
|
6
|
+
for (const a of adapters) {
|
|
7
|
+
if (only && !only.includes(a.name))
|
|
8
|
+
continue;
|
|
9
|
+
try {
|
|
10
|
+
runs.push(...a.scan(opts));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
runs.push({ id: `${a.name}-error`, runtime: a.name, kind: "session", title: `${a.name} adapter failed`, state: "unknown", error: err.message });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return runs;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/** Read up to `bytes` from the start of a file, dropping a torn last line. */
|
|
3
|
+
export function readHeadLines(file, bytes) {
|
|
4
|
+
const fd = fs.openSync(file, "r");
|
|
5
|
+
try {
|
|
6
|
+
const buf = Buffer.alloc(bytes);
|
|
7
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
8
|
+
const text = buf.subarray(0, n).toString("utf8");
|
|
9
|
+
const lines = text.split("\n");
|
|
10
|
+
if (n === bytes)
|
|
11
|
+
lines.pop();
|
|
12
|
+
return parseLines(lines);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
fs.closeSync(fd);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Read up to `bytes` from the end of a file, dropping a torn first line. Agent logs can be hundreds of MB. */
|
|
19
|
+
export function readTailLines(file, bytes) {
|
|
20
|
+
const size = fs.statSync(file).size;
|
|
21
|
+
const start = Math.max(0, size - bytes);
|
|
22
|
+
const fd = fs.openSync(file, "r");
|
|
23
|
+
try {
|
|
24
|
+
const len = size - start;
|
|
25
|
+
const buf = Buffer.alloc(len);
|
|
26
|
+
const n = fs.readSync(fd, buf, 0, len, start);
|
|
27
|
+
const lines = buf.subarray(0, n).toString("utf8").split("\n");
|
|
28
|
+
if (start > 0)
|
|
29
|
+
lines.shift();
|
|
30
|
+
return parseLines(lines);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Raw text from the start of a file, for lines too long to parse whole (inline images). */
|
|
37
|
+
export function readHeadText(file, bytes) {
|
|
38
|
+
const fd = fs.openSync(file, "r");
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.alloc(bytes);
|
|
41
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
42
|
+
return buf.subarray(0, n).toString("utf8");
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Every JSON string value of a "text" key found in raw, possibly truncated, JSON text. */
|
|
49
|
+
export function textValues(raw) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const re = /"text":"((?:[^"\\]|\\.)*)"/g;
|
|
52
|
+
for (let m = re.exec(raw); m; m = re.exec(raw)) {
|
|
53
|
+
try {
|
|
54
|
+
out.push(JSON.parse(`"${m[1]}"`));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// malformed escape; skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function parseLines(lines) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (!line.trim())
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
out.push(JSON.parse(line));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// skip partial or foreign lines
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
export function oneLine(s, max = 100) {
|
|
77
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
78
|
+
return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
|
|
79
|
+
}
|
|
80
|
+
export function isObj(x) {
|
|
81
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
82
|
+
}
|
|
83
|
+
export function str(x) {
|
|
84
|
+
return typeof x === "string" ? x : undefined;
|
|
85
|
+
}
|
|
86
|
+
export const ACTIVITY_MINUTES = 60;
|
|
87
|
+
/** Which of the last `window` minutes saw any of these times: minutes before now, ascending. */
|
|
88
|
+
export function activeMinutes(times, now, window = ACTIVITY_MINUTES) {
|
|
89
|
+
const set = new Set();
|
|
90
|
+
for (const t of times) {
|
|
91
|
+
if (!Number.isFinite(t))
|
|
92
|
+
continue;
|
|
93
|
+
const m = Math.floor((now - t) / 60_000);
|
|
94
|
+
if (m >= 0 && m < window)
|
|
95
|
+
set.add(m);
|
|
96
|
+
}
|
|
97
|
+
return [...set].sort((a, b) => a - b);
|
|
98
|
+
}
|
|
99
|
+
const STAMP = /"timestamp":"([^"]{10,40})"/g;
|
|
100
|
+
/** Every record timestamp in raw log text, in milliseconds. */
|
|
101
|
+
export function timestampsIn(text) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (let m = STAMP.exec(text); m; m = STAMP.exec(text)) {
|
|
104
|
+
const t = Date.parse(m[1]);
|
|
105
|
+
if (Number.isFinite(t))
|
|
106
|
+
out.push(t);
|
|
107
|
+
}
|
|
108
|
+
STAMP.lastIndex = 0;
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
// How far back the first look at a log goes. Later looks read only what was appended since.
|
|
112
|
+
const FIRST_LOOK_BYTES = 16 * 1024 * 1024;
|
|
113
|
+
const seen = new Map();
|
|
114
|
+
/**
|
|
115
|
+
* The active minutes of an append-only log. A long-running process (the desktop app) keeps what
|
|
116
|
+
* it has read, so each refresh reads only the new end of the file.
|
|
117
|
+
*/
|
|
118
|
+
export function logActivity(file, now) {
|
|
119
|
+
const size = fs.statSync(file).size;
|
|
120
|
+
let entry = seen.get(file);
|
|
121
|
+
if (!entry || size < entry.offset)
|
|
122
|
+
entry = { offset: Math.max(0, size - FIRST_LOOK_BYTES), times: [] };
|
|
123
|
+
if (size > entry.offset) {
|
|
124
|
+
const fd = fs.openSync(file, "r");
|
|
125
|
+
try {
|
|
126
|
+
const len = size - entry.offset;
|
|
127
|
+
const buf = Buffer.alloc(len);
|
|
128
|
+
const n = fs.readSync(fd, buf, 0, len, entry.offset);
|
|
129
|
+
const text = buf.subarray(0, n).toString("utf8");
|
|
130
|
+
// stop at the last full line so a record being written is read next time
|
|
131
|
+
const end = text.lastIndexOf("\n") + 1;
|
|
132
|
+
entry.times.push(...timestampsIn(text.slice(0, end)));
|
|
133
|
+
entry.offset += Buffer.byteLength(text.slice(0, end), "utf8");
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
fs.closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const horizon = now - ACTIVITY_MINUTES * 60_000;
|
|
140
|
+
entry.times = entry.times.filter((t) => t >= horizon);
|
|
141
|
+
seen.set(file, entry);
|
|
142
|
+
return activeMinutes(entry.times, now);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=jsonl.js.map
|