consensus-cli 0.1.2 → 0.1.5

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/dist/scan.js CHANGED
@@ -6,6 +6,12 @@ import { execFile } from "child_process";
6
6
  import { promisify } from "util";
7
7
  import { deriveStateWithHold } from "./activity.js";
8
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";
13
+ import { deriveOpenCodeState } from "./opencodeState.js";
14
+ import { deriveClaudeState, summarizeClaudeCommand } from "./claudeCli.js";
9
15
  import { redactText } from "./redact.js";
10
16
  const execFileAsync = promisify(execFile);
11
17
  const repoCache = new Map();
@@ -27,13 +33,50 @@ function isCodexProcess(cmd, name, matchRe) {
27
33
  return true;
28
34
  return false;
29
35
  }
36
+ function isOpenCodeProcess(cmd, name) {
37
+ if (!cmd && !name)
38
+ return false;
39
+ if (name === "opencode")
40
+ return true;
41
+ if (!cmd)
42
+ return false;
43
+ const firstToken = cmd.trim().split(/\s+/)[0];
44
+ const base = path.basename(firstToken);
45
+ if (base === "opencode")
46
+ return true;
47
+ return false;
48
+ }
49
+ function isClaudeProcess(cmd, name) {
50
+ if (!cmd && !name)
51
+ return false;
52
+ if (name === "claude")
53
+ return true;
54
+ if (!cmd)
55
+ return false;
56
+ const firstToken = cmd.trim().split(/\s+/)[0];
57
+ const base = path.basename(firstToken);
58
+ if (base === "claude" || base === "claude.exe")
59
+ return true;
60
+ return false;
61
+ }
30
62
  function inferKind(cmd) {
31
63
  if (cmd.includes(" app-server"))
32
64
  return "app-server";
33
65
  if (cmd.includes(" exec"))
34
66
  return "exec";
35
- if (cmd.includes(" codex") || cmd.startsWith("codex"))
67
+ if (cmd.includes(" codex") || cmd.startsWith("codex") || cmd.includes("/codex"))
36
68
  return "tui";
69
+ if (cmd.includes(" opencode") || cmd.startsWith("opencode") || cmd.includes("/opencode")) {
70
+ if (cmd.includes(" serve") || cmd.includes("--serve") || cmd.includes(" web")) {
71
+ return "opencode-server";
72
+ }
73
+ if (cmd.includes(" run"))
74
+ return "opencode-cli";
75
+ return "opencode-tui";
76
+ }
77
+ const claudeInfo = summarizeClaudeCommand(cmd);
78
+ if (claudeInfo)
79
+ return claudeInfo.kind;
37
80
  return "unknown";
38
81
  }
39
82
  function shortenCmd(cmd, max = 120) {
@@ -44,6 +87,25 @@ function shortenCmd(cmd, max = 120) {
44
87
  }
45
88
  function parseDoingFromCmd(cmd) {
46
89
  const parts = cmd.split(/\s+/g);
90
+ const claudeInfo = summarizeClaudeCommand(cmd);
91
+ if (claudeInfo?.doing)
92
+ return claudeInfo.doing;
93
+ const openIndex = parts.findIndex((part) => part === "opencode" || part.endsWith("/opencode"));
94
+ if (openIndex !== -1) {
95
+ const mode = parts[openIndex + 1];
96
+ if (mode === "serve" || mode === "web")
97
+ return `opencode ${mode}`;
98
+ if (mode === "run") {
99
+ for (let i = openIndex + 2; i < parts.length; i += 1) {
100
+ const part = parts[i];
101
+ if (!part || part.startsWith("-"))
102
+ continue;
103
+ return `opencode run: ${part}`;
104
+ }
105
+ return "opencode run";
106
+ }
107
+ return "opencode";
108
+ }
47
109
  const execIndex = parts.indexOf("exec");
48
110
  if (execIndex !== -1) {
49
111
  for (let i = execIndex + 1; i < parts.length; i += 1) {
@@ -89,12 +151,43 @@ function extractSessionId(cmd) {
89
151
  }
90
152
  return undefined;
91
153
  }
154
+ function extractOpenCodeSessionId(cmd) {
155
+ const parts = cmd.split(/\s+/g);
156
+ const sessionFlag = parts.findIndex((part) => part === "--session" || part === "--session-id" || part === "-s");
157
+ if (sessionFlag !== -1) {
158
+ const token = parts[sessionFlag + 1];
159
+ if (token)
160
+ return token;
161
+ }
162
+ return undefined;
163
+ }
92
164
  function normalizeTitle(value) {
93
165
  if (!value)
94
166
  return undefined;
95
167
  return value.replace(/^prompt:\s*/i, "").trim();
96
168
  }
97
- function deriveTitle(doing, repo, pid) {
169
+ function parseTimestamp(value) {
170
+ if (typeof value === "number" && Number.isFinite(value)) {
171
+ return value < 100000000000 ? value * 1000 : value;
172
+ }
173
+ if (typeof value === "string") {
174
+ const parsed = Date.parse(value);
175
+ if (!Number.isNaN(parsed))
176
+ return parsed;
177
+ }
178
+ return undefined;
179
+ }
180
+ function coerceNumber(value) {
181
+ if (typeof value === "number" && Number.isFinite(value))
182
+ return value;
183
+ if (typeof value === "string") {
184
+ const parsed = Number(value);
185
+ if (!Number.isNaN(parsed))
186
+ return parsed;
187
+ }
188
+ return undefined;
189
+ }
190
+ function deriveTitle(doing, repo, pid, kind) {
98
191
  if (doing) {
99
192
  const trimmed = doing.trim();
100
193
  if (trimmed.startsWith("cmd:"))
@@ -110,7 +203,12 @@ function deriveTitle(doing, repo, pid) {
110
203
  }
111
204
  if (repo)
112
205
  return repo;
113
- return `codex#${pid}`;
206
+ const prefix = kind.startsWith("opencode")
207
+ ? "opencode"
208
+ : kind.startsWith("claude")
209
+ ? "claude"
210
+ : "codex";
211
+ return `${prefix}#${pid}`;
114
212
  }
115
213
  function sanitizeSummary(summary) {
116
214
  if (!summary)
@@ -146,6 +244,35 @@ async function getCwdsForPids(pids) {
146
244
  if (cwd)
147
245
  result.set(pid, cwd);
148
246
  }
247
+ if (result.size > 0) {
248
+ return result;
249
+ }
250
+ }
251
+ catch {
252
+ // fall through to lsof
253
+ }
254
+ try {
255
+ const { stdout } = await execFileAsync("lsof", [
256
+ "-a",
257
+ "-p",
258
+ pids.join(","),
259
+ "-d",
260
+ "cwd",
261
+ "-Fn",
262
+ ]);
263
+ let currentPid = null;
264
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
265
+ for (const line of lines) {
266
+ if (line.startsWith("p")) {
267
+ const pid = Number(line.slice(1));
268
+ currentPid = Number.isNaN(pid) ? null : pid;
269
+ }
270
+ else if (line.startsWith("n") && currentPid) {
271
+ const cwd = line.slice(1).trim();
272
+ if (cwd)
273
+ result.set(currentPid, cwd);
274
+ }
275
+ }
149
276
  }
150
277
  catch {
151
278
  return result;
@@ -210,7 +337,15 @@ export async function scanCodexProcesses() {
210
337
  }
211
338
  const processes = await psList();
212
339
  const codexProcs = processes.filter((proc) => isCodexProcess(proc.cmd, proc.name, matchRe));
213
- const pids = codexProcs.map((proc) => proc.pid);
340
+ const codexPidSet = new Set(codexProcs.map((proc) => proc.pid));
341
+ const opencodeProcs = processes
342
+ .filter((proc) => isOpenCodeProcess(proc.cmd, proc.name))
343
+ .filter((proc) => !codexPidSet.has(proc.pid));
344
+ const opencodePidSet = new Set(opencodeProcs.map((proc) => proc.pid));
345
+ const claudeProcs = processes
346
+ .filter((proc) => isClaudeProcess(proc.cmd, proc.name))
347
+ .filter((proc) => !codexPidSet.has(proc.pid) && !opencodePidSet.has(proc.pid));
348
+ const pids = Array.from(new Set([...codexProcs, ...opencodeProcs, ...claudeProcs].map((proc) => proc.pid)));
214
349
  let usage = {};
215
350
  try {
216
351
  usage = (await pidusage(pids));
@@ -222,6 +357,32 @@ export async function scanCodexProcesses() {
222
357
  const startTimes = await getStartTimesForPids(pids);
223
358
  const codexHome = resolveCodexHome();
224
359
  const sessions = await listRecentSessions(codexHome);
360
+ const opencodeHost = process.env.CONSENSUS_OPENCODE_HOST || "127.0.0.1";
361
+ const opencodePort = Number(process.env.CONSENSUS_OPENCODE_PORT || 4096);
362
+ const opencodeResult = await getOpenCodeSessions(opencodeHost, opencodePort, {
363
+ silent: true,
364
+ timeoutMs: Number(process.env.CONSENSUS_OPENCODE_TIMEOUT_MS || 1000),
365
+ });
366
+ await ensureOpenCodeServer(opencodeHost, opencodePort, opencodeResult);
367
+ if (opencodeProcs.length) {
368
+ ensureOpenCodeEventStream(opencodeHost, opencodePort);
369
+ }
370
+ const opencodeSessions = opencodeResult.ok ? opencodeResult.sessions : [];
371
+ const opencodeApiAvailable = opencodeResult.ok;
372
+ const opencodeSessionsByPid = new Map();
373
+ const opencodeSessionsByDir = new Map();
374
+ for (const session of opencodeSessions) {
375
+ const pid = coerceNumber(session.pid);
376
+ if (typeof pid === "number") {
377
+ opencodeSessionsByPid.set(pid, session);
378
+ }
379
+ if (typeof session.directory === "string") {
380
+ opencodeSessionsByDir.set(session.directory, session);
381
+ }
382
+ if (typeof session.cwd === "string") {
383
+ opencodeSessionsByDir.set(session.cwd, session);
384
+ }
385
+ }
225
386
  const agents = [];
226
387
  const seenIds = new Set();
227
388
  for (const proc of codexProcs) {
@@ -287,7 +448,7 @@ export async function scanCodexProcesses() {
287
448
  const cmdShort = shortenCmd(cmd);
288
449
  const kind = inferKind(cmd);
289
450
  const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
290
- const computedTitle = title || deriveTitle(doing, repoName, proc.pid);
451
+ const computedTitle = title || deriveTitle(doing, repoName, proc.pid, kind);
291
452
  const safeSummary = sanitizeSummary(summary);
292
453
  agents.push({
293
454
  id,
@@ -310,6 +471,181 @@ export async function scanCodexProcesses() {
310
471
  events,
311
472
  });
312
473
  }
474
+ const opencodeEventWindowMs = Number(process.env.CONSENSUS_OPENCODE_EVENT_ACTIVE_MS || 90000);
475
+ const opencodeHoldMs = Number(process.env.CONSENSUS_OPENCODE_ACTIVE_HOLD_MS || 120000);
476
+ const cpuThreshold = Number(process.env.CONSENSUS_CPU_ACTIVE || 1);
477
+ for (const proc of opencodeProcs) {
478
+ const stats = usage[proc.pid] || {};
479
+ const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
480
+ const mem = typeof stats.memory === "number" ? stats.memory : 0;
481
+ const elapsed = stats.elapsed;
482
+ const startMs = typeof elapsed === "number"
483
+ ? Date.now() - elapsed
484
+ : startTimes.get(proc.pid);
485
+ const cmdRaw = proc.cmd || proc.name || "";
486
+ const sessionByPid = opencodeSessionsByPid.get(proc.pid);
487
+ const cwdMatch = cwds.get(proc.pid);
488
+ const sessionByDir = sessionByPid
489
+ ? undefined
490
+ : opencodeSessionsByDir.get(cwdMatch || "");
491
+ const session = sessionByPid || sessionByDir;
492
+ const storageSession = !session && cwdMatch ? await getOpenCodeSessionForDirectory(cwdMatch) : undefined;
493
+ const sessionId = session?.id || storageSession?.id || extractOpenCodeSessionId(cmdRaw);
494
+ const sessionTitle = normalizeTitle(session?.title || session?.name || storageSession?.title);
495
+ const sessionCwd = session?.cwd || session?.directory || storageSession?.directory;
496
+ const cwdRaw = sessionCwd || cwds.get(proc.pid);
497
+ const cwd = redactText(cwdRaw) || cwdRaw;
498
+ const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
499
+ const repoName = repoRoot ? path.basename(repoRoot) : undefined;
500
+ const lastActivityAt = parseTimestamp(session?.lastActivity ||
501
+ session?.lastActivityAt ||
502
+ storageSession?.time?.updated ||
503
+ storageSession?.time?.created ||
504
+ session?.time?.updated ||
505
+ session?.time?.created ||
506
+ session?.updatedAt ||
507
+ session?.updated ||
508
+ session?.createdAt ||
509
+ session?.created);
510
+ const statusRaw = typeof session?.status === "string" ? session.status : undefined;
511
+ const status = statusRaw?.toLowerCase();
512
+ const statusIsError = !!status && /error|failed|failure/.test(status);
513
+ const statusIsIdle = !!status && /idle|stopped|paused/.test(status);
514
+ let hasError = statusIsError;
515
+ const model = typeof session?.model === "string" ? session.model : undefined;
516
+ let doing = sessionTitle;
517
+ let summary;
518
+ let events;
519
+ const eventActivity = getOpenCodeActivityBySession(sessionId) || getOpenCodeActivityByPid(proc.pid);
520
+ let lastEventAt = eventActivity?.lastEventAt;
521
+ let inFlight = eventActivity?.inFlight;
522
+ if (eventActivity) {
523
+ events = eventActivity.events;
524
+ summary = eventActivity.summary || summary;
525
+ lastEventAt = eventActivity.lastEventAt || lastEventAt;
526
+ if (eventActivity.hasError)
527
+ hasError = true;
528
+ if (eventActivity.inFlight)
529
+ inFlight = true;
530
+ if (eventActivity.summary?.current)
531
+ doing = eventActivity.summary.current;
532
+ }
533
+ if (!lastEventAt && statusIsIdle) {
534
+ lastEventAt = undefined;
535
+ }
536
+ if (!doing) {
537
+ doing =
538
+ parseDoingFromCmd(proc.cmd || "") || shortenCmd(proc.cmd || proc.name || "");
539
+ }
540
+ if (doing)
541
+ summary = { current: doing };
542
+ const id = `${proc.pid}`;
543
+ const cached = activityCache.get(id);
544
+ const kind = inferKind(cmdRaw);
545
+ const activity = deriveOpenCodeState({
546
+ cpu,
547
+ hasError,
548
+ lastEventAt: lastEventAt,
549
+ inFlight,
550
+ status,
551
+ isServer: kind === "opencode-server",
552
+ previousActiveAt: cached?.lastActiveAt,
553
+ now,
554
+ cpuThreshold,
555
+ eventWindowMs: opencodeEventWindowMs,
556
+ holdMs: opencodeHoldMs,
557
+ });
558
+ let state = activity.state;
559
+ const hasSignal = statusIsIdle ||
560
+ statusIsError ||
561
+ typeof lastEventAt === "number" ||
562
+ typeof inFlight === "boolean";
563
+ if (!opencodeApiAvailable && !hasSignal)
564
+ state = "idle";
565
+ if (!hasSignal && cpu <= cpuThreshold) {
566
+ state = "idle";
567
+ }
568
+ activityCache.set(id, { lastActiveAt: activity.lastActiveAt, lastSeenAt: now });
569
+ seenIds.add(id);
570
+ const cmd = redactText(cmdRaw) || cmdRaw;
571
+ const cmdShort = shortenCmd(cmd);
572
+ const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
573
+ const computedTitle = sessionTitle || deriveTitle(doing, repoName, proc.pid, kind);
574
+ const safeSummary = sanitizeSummary(summary);
575
+ agents.push({
576
+ id,
577
+ pid: proc.pid,
578
+ startedAt,
579
+ lastEventAt,
580
+ title: redactText(computedTitle) || computedTitle,
581
+ cmd,
582
+ cmdShort,
583
+ kind,
584
+ cpu,
585
+ mem,
586
+ state,
587
+ doing: redactText(doing) || doing,
588
+ repo: repoName,
589
+ cwd,
590
+ model,
591
+ summary: safeSummary,
592
+ events,
593
+ });
594
+ }
595
+ for (const proc of claudeProcs) {
596
+ const stats = usage[proc.pid] || {};
597
+ const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
598
+ const mem = typeof stats.memory === "number" ? stats.memory : 0;
599
+ const elapsed = stats.elapsed;
600
+ const startMs = typeof elapsed === "number"
601
+ ? Date.now() - elapsed
602
+ : startTimes.get(proc.pid);
603
+ const cmdRaw = proc.cmd || proc.name || "";
604
+ const claudeInfo = summarizeClaudeCommand(cmdRaw);
605
+ const doing = claudeInfo?.doing ||
606
+ parseDoingFromCmd(cmdRaw) ||
607
+ shortenCmd(cmdRaw || proc.name || "");
608
+ const summary = doing ? { current: doing } : undefined;
609
+ const model = claudeInfo?.model;
610
+ const cwdRaw = cwds.get(proc.pid);
611
+ const cwd = redactText(cwdRaw) || cwdRaw;
612
+ const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
613
+ const repoName = repoRoot ? path.basename(repoRoot) : undefined;
614
+ const kind = claudeInfo?.kind || inferKind(cmdRaw);
615
+ const id = `${proc.pid}`;
616
+ const cached = activityCache.get(id);
617
+ const activity = deriveClaudeState({
618
+ cpu,
619
+ info: claudeInfo,
620
+ previousActiveAt: cached?.lastActiveAt,
621
+ now,
622
+ });
623
+ const state = activity.state;
624
+ activityCache.set(id, { lastActiveAt: state === "active" ? activity.lastActiveAt : undefined, lastSeenAt: now });
625
+ seenIds.add(id);
626
+ const cmd = redactText(cmdRaw) || cmdRaw;
627
+ const cmdShort = shortenCmd(cmd);
628
+ const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
629
+ const computedTitle = deriveTitle(doing, repoName, proc.pid, kind);
630
+ const safeSummary = sanitizeSummary(summary);
631
+ agents.push({
632
+ id,
633
+ pid: proc.pid,
634
+ startedAt,
635
+ title: redactText(computedTitle) || computedTitle,
636
+ cmd,
637
+ cmdShort,
638
+ kind,
639
+ cpu,
640
+ mem,
641
+ state,
642
+ doing: redactText(doing) || doing,
643
+ repo: repoName,
644
+ cwd,
645
+ model,
646
+ summary: safeSummary,
647
+ });
648
+ }
313
649
  for (const id of activityCache.keys()) {
314
650
  if (!seenIds.has(id)) {
315
651
  activityCache.delete(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consensus-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "repository": {