consensus-cli 0.1.0 → 0.1.4
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/CHANGELOG.md +21 -0
- package/README.md +7 -2
- package/dist/activity.js +20 -3
- package/dist/cli.js +12 -0
- package/dist/codexLogs.js +73 -1
- package/dist/opencodeApi.js +84 -0
- package/dist/opencodeEvents.js +359 -0
- package/dist/opencodeServer.js +91 -0
- package/dist/opencodeStorage.js +127 -0
- package/dist/scan.js +333 -13
- package/package.json +3 -2
- package/public/app.js +162 -25
- package/public/index.html +3 -0
- package/public/style.css +29 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFile, spawn } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { getOpenCodeSessions } from "./opencodeApi.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const AUTOSTART_ENABLED = process.env.CONSENSUS_OPENCODE_AUTOSTART !== "0";
|
|
6
|
+
const CHECK_INTERVAL_MS = 30000;
|
|
7
|
+
const INSTALL_CHECK_INTERVAL_MS = 5 * 60000;
|
|
8
|
+
const START_BACKOFF_MS = 60000;
|
|
9
|
+
let lastAttemptAt = 0;
|
|
10
|
+
let lastInstallCheck = 0;
|
|
11
|
+
let lastStartAt = 0;
|
|
12
|
+
let opencodeInstalled = null;
|
|
13
|
+
let startedPid = null;
|
|
14
|
+
let startInFlight = false;
|
|
15
|
+
async function isOpenCodeInstalled() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
if (opencodeInstalled !== null && now - lastInstallCheck < INSTALL_CHECK_INTERVAL_MS) {
|
|
18
|
+
return opencodeInstalled;
|
|
19
|
+
}
|
|
20
|
+
lastInstallCheck = now;
|
|
21
|
+
try {
|
|
22
|
+
await execFileAsync("opencode", ["--version"]);
|
|
23
|
+
opencodeInstalled = true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error?.code === "ENOENT") {
|
|
27
|
+
opencodeInstalled = false;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
opencodeInstalled = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return opencodeInstalled;
|
|
34
|
+
}
|
|
35
|
+
function spawnOpenCodeServer(host, port) {
|
|
36
|
+
if (startInFlight)
|
|
37
|
+
return;
|
|
38
|
+
startInFlight = true;
|
|
39
|
+
const child = spawn("opencode", ["serve", "--hostname", host, "--port", String(port)], {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
detached: true,
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
startedPid = child.pid ?? null;
|
|
45
|
+
child.on("error", () => {
|
|
46
|
+
startInFlight = false;
|
|
47
|
+
});
|
|
48
|
+
child.on("spawn", () => {
|
|
49
|
+
startInFlight = false;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function ensureOpenCodeServer(host, port, existingResult) {
|
|
53
|
+
if (!AUTOSTART_ENABLED)
|
|
54
|
+
return;
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now - lastAttemptAt < CHECK_INTERVAL_MS)
|
|
57
|
+
return;
|
|
58
|
+
lastAttemptAt = now;
|
|
59
|
+
const installed = await isOpenCodeInstalled();
|
|
60
|
+
if (!installed)
|
|
61
|
+
return;
|
|
62
|
+
const result = existingResult ?? (await getOpenCodeSessions(host, port, { silent: true }));
|
|
63
|
+
if (result.ok)
|
|
64
|
+
return;
|
|
65
|
+
if (result.reachable)
|
|
66
|
+
return;
|
|
67
|
+
if (now - lastStartAt < START_BACKOFF_MS)
|
|
68
|
+
return;
|
|
69
|
+
lastStartAt = now;
|
|
70
|
+
spawnOpenCodeServer(host, port);
|
|
71
|
+
}
|
|
72
|
+
function stopOpenCodeServer() {
|
|
73
|
+
if (!startedPid)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
process.kill(startedPid);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore failures
|
|
80
|
+
}
|
|
81
|
+
startedPid = null;
|
|
82
|
+
}
|
|
83
|
+
process.on("exit", stopOpenCodeServer);
|
|
84
|
+
process.on("SIGINT", () => {
|
|
85
|
+
stopOpenCodeServer();
|
|
86
|
+
process.exit(0);
|
|
87
|
+
});
|
|
88
|
+
process.on("SIGTERM", () => {
|
|
89
|
+
stopOpenCodeServer();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fsp from "fs/promises";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const PROJECT_SCAN_INTERVAL_MS = 60000;
|
|
5
|
+
const SESSION_SCAN_INTERVAL_MS = 5000;
|
|
6
|
+
let projectCache = [];
|
|
7
|
+
let projectCacheAt = 0;
|
|
8
|
+
const sessionCache = new Map();
|
|
9
|
+
export function resolveOpenCodeHome(env = process.env) {
|
|
10
|
+
const override = env.CONSENSUS_OPENCODE_HOME;
|
|
11
|
+
if (override)
|
|
12
|
+
return path.resolve(override);
|
|
13
|
+
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
14
|
+
}
|
|
15
|
+
async function listProjectEntries(home) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
if (now - projectCacheAt < PROJECT_SCAN_INTERVAL_MS)
|
|
18
|
+
return projectCache;
|
|
19
|
+
projectCacheAt = now;
|
|
20
|
+
const projectDir = path.join(home, "storage", "project");
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = await fsp.readdir(projectDir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
projectCache = [];
|
|
27
|
+
return projectCache;
|
|
28
|
+
}
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
32
|
+
continue;
|
|
33
|
+
const fullPath = path.join(projectDir, entry.name);
|
|
34
|
+
try {
|
|
35
|
+
const raw = await fsp.readFile(fullPath, "utf8");
|
|
36
|
+
const data = JSON.parse(raw);
|
|
37
|
+
results.push({
|
|
38
|
+
id: data.id || entry.name.replace(/\.json$/, ""),
|
|
39
|
+
worktree: data.worktree || data.directory,
|
|
40
|
+
time: data.time,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
projectCache = results;
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
function pickProjectId(projects, cwd) {
|
|
51
|
+
const normalized = cwd.replace(/\\/g, "/");
|
|
52
|
+
let best;
|
|
53
|
+
for (const project of projects) {
|
|
54
|
+
if (!project.worktree)
|
|
55
|
+
continue;
|
|
56
|
+
const projectPath = project.worktree.replace(/\\/g, "/");
|
|
57
|
+
if (normalized === projectPath || normalized.startsWith(`${projectPath}/`)) {
|
|
58
|
+
if (!best || (projectPath.length > (best.worktree?.length || 0))) {
|
|
59
|
+
best = project;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return best?.id;
|
|
64
|
+
}
|
|
65
|
+
async function readLatestSessionForProject(home, projectId) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const cached = sessionCache.get(projectId);
|
|
68
|
+
if (cached && now - cached.scannedAt < SESSION_SCAN_INTERVAL_MS) {
|
|
69
|
+
return cached.session;
|
|
70
|
+
}
|
|
71
|
+
const sessionDir = path.join(home, "storage", "session", projectId);
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await fsp.readdir(sessionDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
sessionCache.set(projectId, { session: undefined, scannedAt: now });
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
let latestPath = null;
|
|
81
|
+
let latestMtime = 0;
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isFile() || !entry.name.startsWith("ses_") || !entry.name.endsWith(".json"))
|
|
84
|
+
continue;
|
|
85
|
+
const fullPath = path.join(sessionDir, entry.name);
|
|
86
|
+
try {
|
|
87
|
+
const stat = await fsp.stat(fullPath);
|
|
88
|
+
if (stat.mtimeMs > latestMtime) {
|
|
89
|
+
latestMtime = stat.mtimeMs;
|
|
90
|
+
latestPath = fullPath;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!latestPath) {
|
|
98
|
+
sessionCache.set(projectId, { session: undefined, scannedAt: now });
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const raw = await fsp.readFile(latestPath, "utf8");
|
|
103
|
+
const data = JSON.parse(raw);
|
|
104
|
+
const session = {
|
|
105
|
+
id: data.id,
|
|
106
|
+
title: data.title,
|
|
107
|
+
directory: data.directory,
|
|
108
|
+
time: data.time,
|
|
109
|
+
};
|
|
110
|
+
sessionCache.set(projectId, { session, scannedAt: now });
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
sessionCache.set(projectId, { session: undefined, scannedAt: now });
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function getOpenCodeSessionForDirectory(cwd, env = process.env) {
|
|
119
|
+
if (!cwd)
|
|
120
|
+
return undefined;
|
|
121
|
+
const home = resolveOpenCodeHome(env);
|
|
122
|
+
const projects = await listProjectEntries(home);
|
|
123
|
+
const projectId = pickProjectId(projects, cwd);
|
|
124
|
+
if (!projectId)
|
|
125
|
+
return undefined;
|
|
126
|
+
return await readLatestSessionForProject(home, projectId);
|
|
127
|
+
}
|
package/dist/scan.js
CHANGED
|
@@ -4,11 +4,16 @@ import fs from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import {
|
|
8
|
-
import { listRecentSessions, pickSessionForProcess, resolveCodexHome, summarizeTail, updateTail, } from "./codexLogs.js";
|
|
7
|
+
import { deriveStateWithHold } from "./activity.js";
|
|
8
|
+
import { listRecentSessions, findSessionById, pickSessionForProcess, resolveCodexHome, summarizeTail, updateTail, } from "./codexLogs.js";
|
|
9
|
+
import { getOpenCodeSessions } from "./opencodeApi.js";
|
|
10
|
+
import { ensureOpenCodeServer } from "./opencodeServer.js";
|
|
11
|
+
import { ensureOpenCodeEventStream, getOpenCodeActivityByPid, getOpenCodeActivityBySession, } from "./opencodeEvents.js";
|
|
12
|
+
import { getOpenCodeSessionForDirectory } from "./opencodeStorage.js";
|
|
9
13
|
import { redactText } from "./redact.js";
|
|
10
14
|
const execFileAsync = promisify(execFile);
|
|
11
15
|
const repoCache = new Map();
|
|
16
|
+
const activityCache = new Map();
|
|
12
17
|
function isCodexProcess(cmd, name, matchRe) {
|
|
13
18
|
if (!cmd && !name)
|
|
14
19
|
return false;
|
|
@@ -16,6 +21,8 @@ function isCodexProcess(cmd, name, matchRe) {
|
|
|
16
21
|
return matchRe.test(cmd || "") || matchRe.test(name || "");
|
|
17
22
|
}
|
|
18
23
|
const cmdLine = cmd || "";
|
|
24
|
+
if (cmdLine.includes("/codex/vendor/"))
|
|
25
|
+
return false;
|
|
19
26
|
if (name === "codex")
|
|
20
27
|
return true;
|
|
21
28
|
if (cmdLine === "codex" || cmdLine.startsWith("codex "))
|
|
@@ -24,13 +31,34 @@ function isCodexProcess(cmd, name, matchRe) {
|
|
|
24
31
|
return true;
|
|
25
32
|
return false;
|
|
26
33
|
}
|
|
34
|
+
function isOpenCodeProcess(cmd, name) {
|
|
35
|
+
if (!cmd && !name)
|
|
36
|
+
return false;
|
|
37
|
+
if (name === "opencode")
|
|
38
|
+
return true;
|
|
39
|
+
if (!cmd)
|
|
40
|
+
return false;
|
|
41
|
+
const firstToken = cmd.trim().split(/\s+/)[0];
|
|
42
|
+
const base = path.basename(firstToken);
|
|
43
|
+
if (base === "opencode")
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
27
47
|
function inferKind(cmd) {
|
|
28
48
|
if (cmd.includes(" app-server"))
|
|
29
49
|
return "app-server";
|
|
30
50
|
if (cmd.includes(" exec"))
|
|
31
51
|
return "exec";
|
|
32
|
-
if (cmd.includes(" codex") || cmd.startsWith("codex"))
|
|
52
|
+
if (cmd.includes(" codex") || cmd.startsWith("codex") || cmd.includes("/codex"))
|
|
33
53
|
return "tui";
|
|
54
|
+
if (cmd.includes(" opencode") || cmd.startsWith("opencode") || cmd.includes("/opencode")) {
|
|
55
|
+
if (cmd.includes(" serve") || cmd.includes("--serve") || cmd.includes(" web")) {
|
|
56
|
+
return "opencode-server";
|
|
57
|
+
}
|
|
58
|
+
if (cmd.includes(" run"))
|
|
59
|
+
return "opencode-cli";
|
|
60
|
+
return "opencode-tui";
|
|
61
|
+
}
|
|
34
62
|
return "unknown";
|
|
35
63
|
}
|
|
36
64
|
function shortenCmd(cmd, max = 120) {
|
|
@@ -41,6 +69,22 @@ function shortenCmd(cmd, max = 120) {
|
|
|
41
69
|
}
|
|
42
70
|
function parseDoingFromCmd(cmd) {
|
|
43
71
|
const parts = cmd.split(/\s+/g);
|
|
72
|
+
const openIndex = parts.findIndex((part) => part === "opencode" || part.endsWith("/opencode"));
|
|
73
|
+
if (openIndex !== -1) {
|
|
74
|
+
const mode = parts[openIndex + 1];
|
|
75
|
+
if (mode === "serve" || mode === "web")
|
|
76
|
+
return `opencode ${mode}`;
|
|
77
|
+
if (mode === "run") {
|
|
78
|
+
for (let i = openIndex + 2; i < parts.length; i += 1) {
|
|
79
|
+
const part = parts[i];
|
|
80
|
+
if (!part || part.startsWith("-"))
|
|
81
|
+
continue;
|
|
82
|
+
return `opencode run: ${part}`;
|
|
83
|
+
}
|
|
84
|
+
return "opencode run";
|
|
85
|
+
}
|
|
86
|
+
return "opencode";
|
|
87
|
+
}
|
|
44
88
|
const execIndex = parts.indexOf("exec");
|
|
45
89
|
if (execIndex !== -1) {
|
|
46
90
|
for (let i = execIndex + 1; i < parts.length; i += 1) {
|
|
@@ -70,12 +114,59 @@ function parseDoingFromCmd(cmd) {
|
|
|
70
114
|
return "codex";
|
|
71
115
|
return undefined;
|
|
72
116
|
}
|
|
117
|
+
function extractSessionId(cmd) {
|
|
118
|
+
const parts = cmd.split(/\s+/g);
|
|
119
|
+
const resumeIndex = parts.indexOf("resume");
|
|
120
|
+
if (resumeIndex !== -1) {
|
|
121
|
+
const token = parts[resumeIndex + 1];
|
|
122
|
+
if (token && /^[0-9a-fA-F-]{16,}$/.test(token))
|
|
123
|
+
return token;
|
|
124
|
+
}
|
|
125
|
+
const sessionFlag = parts.findIndex((part) => part === "--session" || part === "--session-id");
|
|
126
|
+
if (sessionFlag !== -1) {
|
|
127
|
+
const token = parts[sessionFlag + 1];
|
|
128
|
+
if (token && /^[0-9a-fA-F-]{16,}$/.test(token))
|
|
129
|
+
return token;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function extractOpenCodeSessionId(cmd) {
|
|
134
|
+
const parts = cmd.split(/\s+/g);
|
|
135
|
+
const sessionFlag = parts.findIndex((part) => part === "--session" || part === "--session-id" || part === "-s");
|
|
136
|
+
if (sessionFlag !== -1) {
|
|
137
|
+
const token = parts[sessionFlag + 1];
|
|
138
|
+
if (token)
|
|
139
|
+
return token;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
73
143
|
function normalizeTitle(value) {
|
|
74
144
|
if (!value)
|
|
75
145
|
return undefined;
|
|
76
146
|
return value.replace(/^prompt:\s*/i, "").trim();
|
|
77
147
|
}
|
|
78
|
-
function
|
|
148
|
+
function parseTimestamp(value) {
|
|
149
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
150
|
+
return value < 100000000000 ? value * 1000 : value;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
const parsed = Date.parse(value);
|
|
154
|
+
if (!Number.isNaN(parsed))
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function coerceNumber(value) {
|
|
160
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
161
|
+
return value;
|
|
162
|
+
if (typeof value === "string") {
|
|
163
|
+
const parsed = Number(value);
|
|
164
|
+
if (!Number.isNaN(parsed))
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function deriveTitle(doing, repo, pid, kind) {
|
|
79
170
|
if (doing) {
|
|
80
171
|
const trimmed = doing.trim();
|
|
81
172
|
if (trimmed.startsWith("cmd:"))
|
|
@@ -91,7 +182,8 @@ function deriveTitle(doing, repo, pid) {
|
|
|
91
182
|
}
|
|
92
183
|
if (repo)
|
|
93
184
|
return repo;
|
|
94
|
-
|
|
185
|
+
const prefix = kind.startsWith("opencode") ? "opencode" : "codex";
|
|
186
|
+
return `${prefix}#${pid}`;
|
|
95
187
|
}
|
|
96
188
|
function sanitizeSummary(summary) {
|
|
97
189
|
if (!summary)
|
|
@@ -127,6 +219,61 @@ async function getCwdsForPids(pids) {
|
|
|
127
219
|
if (cwd)
|
|
128
220
|
result.set(pid, cwd);
|
|
129
221
|
}
|
|
222
|
+
if (result.size > 0) {
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// fall through to lsof
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const { stdout } = await execFileAsync("lsof", [
|
|
231
|
+
"-a",
|
|
232
|
+
"-p",
|
|
233
|
+
pids.join(","),
|
|
234
|
+
"-d",
|
|
235
|
+
"cwd",
|
|
236
|
+
"-Fn",
|
|
237
|
+
]);
|
|
238
|
+
let currentPid = null;
|
|
239
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
if (line.startsWith("p")) {
|
|
242
|
+
const pid = Number(line.slice(1));
|
|
243
|
+
currentPid = Number.isNaN(pid) ? null : pid;
|
|
244
|
+
}
|
|
245
|
+
else if (line.startsWith("n") && currentPid) {
|
|
246
|
+
const cwd = line.slice(1).trim();
|
|
247
|
+
if (cwd)
|
|
248
|
+
result.set(currentPid, cwd);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
async function getStartTimesForPids(pids) {
|
|
258
|
+
const result = new Map();
|
|
259
|
+
if (pids.length === 0)
|
|
260
|
+
return result;
|
|
261
|
+
if (process.platform === "win32")
|
|
262
|
+
return result;
|
|
263
|
+
try {
|
|
264
|
+
const { stdout } = await execFileAsync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")]);
|
|
265
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
268
|
+
if (!match)
|
|
269
|
+
continue;
|
|
270
|
+
const pid = Number(match[1]);
|
|
271
|
+
const dateStr = match[2].trim();
|
|
272
|
+
const parsed = Date.parse(dateStr);
|
|
273
|
+
if (!Number.isNaN(parsed)) {
|
|
274
|
+
result.set(pid, parsed);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
130
277
|
}
|
|
131
278
|
catch {
|
|
132
279
|
return result;
|
|
@@ -152,6 +299,7 @@ function findRepoRoot(cwd) {
|
|
|
152
299
|
return null;
|
|
153
300
|
}
|
|
154
301
|
export async function scanCodexProcesses() {
|
|
302
|
+
const now = Date.now();
|
|
155
303
|
const matchEnv = process.env.CONSENSUS_PROCESS_MATCH;
|
|
156
304
|
let matchRe;
|
|
157
305
|
if (matchEnv) {
|
|
@@ -164,7 +312,11 @@ export async function scanCodexProcesses() {
|
|
|
164
312
|
}
|
|
165
313
|
const processes = await psList();
|
|
166
314
|
const codexProcs = processes.filter((proc) => isCodexProcess(proc.cmd, proc.name, matchRe));
|
|
167
|
-
const
|
|
315
|
+
const codexPidSet = new Set(codexProcs.map((proc) => proc.pid));
|
|
316
|
+
const opencodeProcs = processes
|
|
317
|
+
.filter((proc) => isOpenCodeProcess(proc.cmd, proc.name))
|
|
318
|
+
.filter((proc) => !codexPidSet.has(proc.pid));
|
|
319
|
+
const pids = Array.from(new Set([...codexProcs, ...opencodeProcs].map((proc) => proc.pid)));
|
|
168
320
|
let usage = {};
|
|
169
321
|
try {
|
|
170
322
|
usage = (await pidusage(pids));
|
|
@@ -173,16 +325,49 @@ export async function scanCodexProcesses() {
|
|
|
173
325
|
usage = {};
|
|
174
326
|
}
|
|
175
327
|
const cwds = await getCwdsForPids(pids);
|
|
328
|
+
const startTimes = await getStartTimesForPids(pids);
|
|
176
329
|
const codexHome = resolveCodexHome();
|
|
177
330
|
const sessions = await listRecentSessions(codexHome);
|
|
331
|
+
const opencodeHost = process.env.CONSENSUS_OPENCODE_HOST || "127.0.0.1";
|
|
332
|
+
const opencodePort = Number(process.env.CONSENSUS_OPENCODE_PORT || 4096);
|
|
333
|
+
const opencodeResult = await getOpenCodeSessions(opencodeHost, opencodePort, {
|
|
334
|
+
silent: true,
|
|
335
|
+
});
|
|
336
|
+
await ensureOpenCodeServer(opencodeHost, opencodePort, opencodeResult);
|
|
337
|
+
if (opencodeProcs.length) {
|
|
338
|
+
ensureOpenCodeEventStream(opencodeHost, opencodePort);
|
|
339
|
+
}
|
|
340
|
+
const opencodeSessions = opencodeResult.ok ? opencodeResult.sessions : [];
|
|
341
|
+
const opencodeApiAvailable = opencodeResult.ok;
|
|
342
|
+
const opencodeSessionsByPid = new Map();
|
|
343
|
+
const opencodeSessionsByDir = new Map();
|
|
344
|
+
for (const session of opencodeSessions) {
|
|
345
|
+
const pid = coerceNumber(session.pid);
|
|
346
|
+
if (typeof pid === "number") {
|
|
347
|
+
opencodeSessionsByPid.set(pid, session);
|
|
348
|
+
}
|
|
349
|
+
if (typeof session.directory === "string") {
|
|
350
|
+
opencodeSessionsByDir.set(session.directory, session);
|
|
351
|
+
}
|
|
352
|
+
if (typeof session.cwd === "string") {
|
|
353
|
+
opencodeSessionsByDir.set(session.cwd, session);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
178
356
|
const agents = [];
|
|
357
|
+
const seenIds = new Set();
|
|
179
358
|
for (const proc of codexProcs) {
|
|
180
359
|
const stats = usage[proc.pid] || {};
|
|
181
360
|
const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
|
|
182
361
|
const mem = typeof stats.memory === "number" ? stats.memory : 0;
|
|
183
362
|
const elapsed = stats.elapsed;
|
|
184
|
-
const startMs = typeof elapsed === "number"
|
|
185
|
-
|
|
363
|
+
const startMs = typeof elapsed === "number"
|
|
364
|
+
? Date.now() - elapsed
|
|
365
|
+
: startTimes.get(proc.pid);
|
|
366
|
+
const cmdRaw = proc.cmd || proc.name || "";
|
|
367
|
+
const sessionId = extractSessionId(cmdRaw);
|
|
368
|
+
const session = (sessionId && sessions.find((item) => item.path.includes(sessionId))) ||
|
|
369
|
+
(sessionId ? await findSessionById(codexHome, sessionId) : undefined) ||
|
|
370
|
+
pickSessionForProcess(sessions, startMs);
|
|
186
371
|
let doing;
|
|
187
372
|
let events;
|
|
188
373
|
let model;
|
|
@@ -190,6 +375,7 @@ export async function scanCodexProcesses() {
|
|
|
190
375
|
let title;
|
|
191
376
|
let summary;
|
|
192
377
|
let lastEventAt;
|
|
378
|
+
let inFlight = false;
|
|
193
379
|
if (session) {
|
|
194
380
|
const tail = await updateTail(session.path);
|
|
195
381
|
if (tail) {
|
|
@@ -201,6 +387,7 @@ export async function scanCodexProcesses() {
|
|
|
201
387
|
title = normalizeTitle(tailSummary.title);
|
|
202
388
|
summary = tailSummary.summary;
|
|
203
389
|
lastEventAt = tailSummary.lastEventAt;
|
|
390
|
+
inFlight = !!tailSummary.inFlight;
|
|
204
391
|
}
|
|
205
392
|
}
|
|
206
393
|
if (!doing) {
|
|
@@ -214,14 +401,24 @@ export async function scanCodexProcesses() {
|
|
|
214
401
|
const cwd = redactText(cwdRaw) || cwdRaw;
|
|
215
402
|
const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
|
|
216
403
|
const repoName = repoRoot ? path.basename(repoRoot) : undefined;
|
|
217
|
-
const
|
|
218
|
-
const
|
|
404
|
+
const id = `${proc.pid}`;
|
|
405
|
+
const cached = activityCache.get(id);
|
|
406
|
+
const activity = deriveStateWithHold({
|
|
407
|
+
cpu,
|
|
408
|
+
hasError,
|
|
409
|
+
lastEventAt,
|
|
410
|
+
inFlight,
|
|
411
|
+
previousActiveAt: cached?.lastActiveAt,
|
|
412
|
+
now,
|
|
413
|
+
});
|
|
414
|
+
const state = activity.state;
|
|
415
|
+
activityCache.set(id, { lastActiveAt: activity.lastActiveAt, lastSeenAt: now });
|
|
416
|
+
seenIds.add(id);
|
|
219
417
|
const cmd = redactText(cmdRaw) || cmdRaw;
|
|
220
418
|
const cmdShort = shortenCmd(cmd);
|
|
221
419
|
const kind = inferKind(cmd);
|
|
222
|
-
const id = `${proc.pid}`;
|
|
223
420
|
const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
|
|
224
|
-
const computedTitle = title || deriveTitle(doing, repoName, proc.pid);
|
|
421
|
+
const computedTitle = title || deriveTitle(doing, repoName, proc.pid, kind);
|
|
225
422
|
const safeSummary = sanitizeSummary(summary);
|
|
226
423
|
agents.push({
|
|
227
424
|
id,
|
|
@@ -244,7 +441,130 @@ export async function scanCodexProcesses() {
|
|
|
244
441
|
events,
|
|
245
442
|
});
|
|
246
443
|
}
|
|
247
|
-
|
|
444
|
+
for (const proc of opencodeProcs) {
|
|
445
|
+
const stats = usage[proc.pid] || {};
|
|
446
|
+
const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
|
|
447
|
+
const mem = typeof stats.memory === "number" ? stats.memory : 0;
|
|
448
|
+
const elapsed = stats.elapsed;
|
|
449
|
+
const startMs = typeof elapsed === "number"
|
|
450
|
+
? Date.now() - elapsed
|
|
451
|
+
: startTimes.get(proc.pid);
|
|
452
|
+
const cmdRaw = proc.cmd || proc.name || "";
|
|
453
|
+
const sessionByPid = opencodeSessionsByPid.get(proc.pid);
|
|
454
|
+
const cwdMatch = cwds.get(proc.pid);
|
|
455
|
+
const sessionByDir = sessionByPid
|
|
456
|
+
? undefined
|
|
457
|
+
: opencodeSessionsByDir.get(cwdMatch || "");
|
|
458
|
+
const session = sessionByPid || sessionByDir;
|
|
459
|
+
const storageSession = !session && cwdMatch ? await getOpenCodeSessionForDirectory(cwdMatch) : undefined;
|
|
460
|
+
const sessionId = session?.id || storageSession?.id || extractOpenCodeSessionId(cmdRaw);
|
|
461
|
+
const sessionTitle = normalizeTitle(session?.title || session?.name || storageSession?.title);
|
|
462
|
+
const sessionCwd = session?.cwd || session?.directory || storageSession?.directory;
|
|
463
|
+
const cwdRaw = sessionCwd || cwds.get(proc.pid);
|
|
464
|
+
const cwd = redactText(cwdRaw) || cwdRaw;
|
|
465
|
+
const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
|
|
466
|
+
const repoName = repoRoot ? path.basename(repoRoot) : undefined;
|
|
467
|
+
let lastEventAt = parseTimestamp(session?.lastActivity ||
|
|
468
|
+
session?.lastActivityAt ||
|
|
469
|
+
storageSession?.time?.updated ||
|
|
470
|
+
storageSession?.time?.created ||
|
|
471
|
+
session?.time?.updated ||
|
|
472
|
+
session?.time?.created ||
|
|
473
|
+
session?.updatedAt ||
|
|
474
|
+
session?.updated ||
|
|
475
|
+
session?.createdAt ||
|
|
476
|
+
session?.created);
|
|
477
|
+
const statusRaw = typeof session?.status === "string" ? session.status : undefined;
|
|
478
|
+
const status = statusRaw?.toLowerCase();
|
|
479
|
+
const statusIsError = !!status && /error|failed|failure/.test(status);
|
|
480
|
+
const statusIsActive = !!status && /running|active|processing/.test(status);
|
|
481
|
+
const statusIsIdle = !!status && /idle|stopped|paused/.test(status);
|
|
482
|
+
let hasError = statusIsError;
|
|
483
|
+
let inFlight = statusIsActive;
|
|
484
|
+
const model = typeof session?.model === "string" ? session.model : undefined;
|
|
485
|
+
let doing = sessionTitle;
|
|
486
|
+
let summary;
|
|
487
|
+
let events;
|
|
488
|
+
const eventActivity = getOpenCodeActivityBySession(sessionId) || getOpenCodeActivityByPid(proc.pid);
|
|
489
|
+
if (eventActivity) {
|
|
490
|
+
events = eventActivity.events;
|
|
491
|
+
summary = eventActivity.summary || summary;
|
|
492
|
+
lastEventAt = eventActivity.lastEventAt || lastEventAt;
|
|
493
|
+
if (eventActivity.hasError)
|
|
494
|
+
hasError = true;
|
|
495
|
+
if (eventActivity.inFlight)
|
|
496
|
+
inFlight = true;
|
|
497
|
+
if (eventActivity.summary?.current)
|
|
498
|
+
doing = eventActivity.summary.current;
|
|
499
|
+
}
|
|
500
|
+
if (!doing) {
|
|
501
|
+
doing =
|
|
502
|
+
parseDoingFromCmd(proc.cmd || "") || shortenCmd(proc.cmd || proc.name || "");
|
|
503
|
+
}
|
|
504
|
+
if (doing)
|
|
505
|
+
summary = { current: doing };
|
|
506
|
+
const id = `${proc.pid}`;
|
|
507
|
+
const cached = activityCache.get(id);
|
|
508
|
+
const activity = deriveStateWithHold({
|
|
509
|
+
cpu,
|
|
510
|
+
hasError,
|
|
511
|
+
lastEventAt,
|
|
512
|
+
inFlight,
|
|
513
|
+
previousActiveAt: cached?.lastActiveAt,
|
|
514
|
+
now,
|
|
515
|
+
});
|
|
516
|
+
let state = activity.state;
|
|
517
|
+
if (statusIsError)
|
|
518
|
+
state = "error";
|
|
519
|
+
else if (statusIsActive)
|
|
520
|
+
state = "active";
|
|
521
|
+
else if (statusIsIdle)
|
|
522
|
+
state = "idle";
|
|
523
|
+
const hasSignal = statusIsActive ||
|
|
524
|
+
statusIsIdle ||
|
|
525
|
+
statusIsError ||
|
|
526
|
+
typeof lastEventAt === "number" ||
|
|
527
|
+
!!inFlight;
|
|
528
|
+
if (!opencodeApiAvailable && !hasSignal)
|
|
529
|
+
state = "idle";
|
|
530
|
+
const cpuThreshold = Number(process.env.CONSENSUS_CPU_ACTIVE || 1);
|
|
531
|
+
if (!hasSignal && cpu <= cpuThreshold) {
|
|
532
|
+
state = "idle";
|
|
533
|
+
}
|
|
534
|
+
activityCache.set(id, { lastActiveAt: state === "active" ? activity.lastActiveAt : undefined, lastSeenAt: now });
|
|
535
|
+
seenIds.add(id);
|
|
536
|
+
const cmd = redactText(cmdRaw) || cmdRaw;
|
|
537
|
+
const cmdShort = shortenCmd(cmd);
|
|
538
|
+
const kind = inferKind(cmd);
|
|
539
|
+
const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
|
|
540
|
+
const computedTitle = sessionTitle || deriveTitle(doing, repoName, proc.pid, kind);
|
|
541
|
+
const safeSummary = sanitizeSummary(summary);
|
|
542
|
+
agents.push({
|
|
543
|
+
id,
|
|
544
|
+
pid: proc.pid,
|
|
545
|
+
startedAt,
|
|
546
|
+
lastEventAt,
|
|
547
|
+
title: redactText(computedTitle) || computedTitle,
|
|
548
|
+
cmd,
|
|
549
|
+
cmdShort,
|
|
550
|
+
kind,
|
|
551
|
+
cpu,
|
|
552
|
+
mem,
|
|
553
|
+
state,
|
|
554
|
+
doing: redactText(doing) || doing,
|
|
555
|
+
repo: repoName,
|
|
556
|
+
cwd,
|
|
557
|
+
model,
|
|
558
|
+
summary: safeSummary,
|
|
559
|
+
events,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
for (const id of activityCache.keys()) {
|
|
563
|
+
if (!seenIds.has(id)) {
|
|
564
|
+
activityCache.delete(id);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return { ts: now, agents };
|
|
248
568
|
}
|
|
249
569
|
const isDirectRun = process.argv[1] && process.argv[1].endsWith("scan.js");
|
|
250
570
|
if (isDirectRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "consensus-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "dist/server.js",
|
|
16
16
|
"bin": {
|
|
17
|
-
"consensus": "dist/cli.js"
|
|
17
|
+
"consensus": "dist/cli.js",
|
|
18
|
+
"consensus-cli": "dist/cli.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|