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.
@@ -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 { deriveState } from "./activity.js";
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 deriveTitle(doing, repo, pid) {
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
- return `codex#${pid}`;
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 pids = codexProcs.map((proc) => proc.pid);
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" ? Date.now() - elapsed : undefined;
185
- const session = pickSessionForProcess(sessions, startMs);
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 state = deriveState({ cpu, hasError, lastEventAt });
218
- const cmdRaw = proc.cmd || proc.name || "";
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
- return { ts: Date.now(), agents };
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.0",
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",