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/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ This project follows Semantic Versioning.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.1.5 - 2026-01-25
9
+ - Add Claude Code process detection with prompt/resume parsing.
10
+ - Apply CLI-specific palettes (Codex/OpenCode/Claude Code) across tiles and lane items.
11
+ - Add Claude CLI parsing unit tests.
12
+ - Update README with Claude Code support.
13
+
14
+ ## 0.1.4 - 2026-01-24
15
+ - Fix OpenCode event tracking build error (pid activity typing).
16
+
17
+ ## 0.1.3 - 2026-01-24
18
+ - Add OpenCode integration (API sessions, event stream, storage fallback).
19
+ - Autostart OpenCode server with opt-out and CLI flags.
20
+ - Split servers into a dedicated lane with distinct palette.
21
+ - Improve layout keys to prevent tile overlap.
22
+ - Add OpenCode unit/integration tests and configuration docs.
23
+
8
24
  ## 0.1.2 - 2026-01-24
9
25
  - Lower CPU threshold for active detection.
10
26
  - Increase activity window defaults for long-running turns.
package/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # consensus-cli
2
2
 
3
- Live isometric atlas for Codex CLI processes, rendered in a local browser.
3
+ [![npm](https://img.shields.io/npm/v/consensus-cli.svg?color=0f766e)](https://www.npmjs.com/package/consensus-cli)
4
+ [![GitHub release](https://img.shields.io/github/v/release/integrate-your-mind/consensus-cli?display_name=tag&color=2563eb)](https://github.com/integrate-your-mind/consensus-cli/releases)
5
+ [![License](https://img.shields.io/npm/l/consensus-cli.svg?color=6b7280)](LICENSE)
6
+
7
+ Live isometric atlas for Codex, OpenCode, and Claude Code sessions, rendered in a local browser.
4
8
 
5
9
  ## Status
6
10
  Beta. Local-only, no hosted service.
7
11
 
8
12
  ## Who it's for
9
- Developers running multiple Codex sessions who want a visual, at-a-glance view of activity.
13
+ Developers running multiple Codex, OpenCode, or Claude Code sessions who want a visual, at-a-glance view of activity.
10
14
 
11
15
  ## Core use cases
12
16
  - Track which agents are active right now.
@@ -15,7 +19,7 @@ Developers running multiple Codex sessions who want a visual, at-a-glance view o
15
19
 
16
20
  ## Scope (non-goals)
17
21
  - Does not start, stop, or manage processes.
18
- - Does not connect to remote Codex instances.
22
+ - Does not connect to remote Codex, OpenCode, or Claude Code instances.
19
23
  - No authentication or multi-user access.
20
24
 
21
25
  ## Quickstart
@@ -27,6 +31,8 @@ npm run dev
27
31
  The server prints the local URL (default `http://127.0.0.1:8787`).
28
32
  Consensus reads local Codex CLI sessions and does not require API keys.
29
33
  You just need Codex CLI installed and signed in (Pro subscription or team plan).
34
+ If OpenCode is installed, Consensus will auto-start its local server.
35
+ If Claude Code is installed, it will appear automatically (run `claude` once to sign in).
30
36
 
31
37
  ## Run via npx
32
38
  ```bash
@@ -39,17 +45,19 @@ consensus dev server running on http://127.0.0.1:8787
39
45
  ```
40
46
 
41
47
  ## What you get
42
- - One tile per running `codex` process.
48
+ - One tile per running `codex`, `opencode`, or `claude` process.
43
49
  - Activity state (active/idle/error) from CPU and recent events.
44
- - Best-effort "doing" summary from Codex session JSONL.
50
+ - Best-effort "doing" summary from Codex session JSONL, OpenCode events, or Claude CLI flags.
45
51
  - Click a tile for details and recent events.
46
- - Active lane that highlights what is working right now.
52
+ - Active lane for agents plus a dedicated lane for servers.
47
53
 
48
54
  ## How it works
49
- 1) Scan OS process list for Codex.
50
- 2) Resolve recent session JSONL under `CODEX_HOME/sessions/`.
51
- 3) Poll and push snapshots over WebSocket.
52
- 4) Render tiles on a canvas with isometric projection.
55
+ 1) Scan OS process list for Codex + OpenCode + Claude Code.
56
+ 2) Resolve Codex session JSONL under `CODEX_HOME/sessions/`.
57
+ 3) Query the OpenCode local server API and event stream (with storage fallback).
58
+ 4) Read Claude Code CLI flags to infer current work.
59
+ 5) Poll and push snapshots over WebSocket.
60
+ 6) Render tiles on a canvas with isometric projection.
53
61
 
54
62
  ## Install options
55
63
  - Local dev: `npm install` + `npm run dev`
@@ -62,6 +70,10 @@ consensus dev server running on http://127.0.0.1:8787
62
70
  - `CONSENSUS_PORT`: server port (default `8787`).
63
71
  - `CONSENSUS_POLL_MS`: polling interval in ms (default `1000`).
64
72
  - `CONSENSUS_CODEX_HOME`: override Codex home (default `~/.codex`).
73
+ - `CONSENSUS_OPENCODE_HOST`: OpenCode server host (default `127.0.0.1`).
74
+ - `CONSENSUS_OPENCODE_PORT`: OpenCode server port (default `4096`).
75
+ - `CONSENSUS_OPENCODE_AUTOSTART`: set to `0` to disable OpenCode autostart.
76
+ - `CONSENSUS_OPENCODE_EVENTS`: set to `0` to disable OpenCode event stream.
65
77
  - `CONSENSUS_PROCESS_MATCH`: regex to match codex processes.
66
78
  - `CONSENSUS_REDACT_PII`: set to `0` to disable redaction (default enabled).
67
79
  - `CONSENSUS_EVENT_ACTIVE_MS`: active window after last event in ms (default `300000`).
@@ -0,0 +1,125 @@
1
+ import { deriveStateWithHold } from "./activity.js";
2
+ const CLAUDE_BINARIES = new Set(["claude", "claude.exe"]);
3
+ export function splitArgs(command) {
4
+ if (!command)
5
+ return [];
6
+ const args = [];
7
+ const regex = /"([^"]*)"|'([^']*)'|\S+/g;
8
+ let match;
9
+ while ((match = regex.exec(command)) !== null) {
10
+ const token = match[1] ?? match[2] ?? match[0];
11
+ if (token)
12
+ args.push(token);
13
+ }
14
+ return args;
15
+ }
16
+ function findClaudeIndex(parts) {
17
+ for (let i = 0; i < parts.length; i += 1) {
18
+ const base = parts[i] ? parts[i].split(/[/\\]/).pop() || "" : "";
19
+ if (CLAUDE_BINARIES.has(base))
20
+ return i;
21
+ }
22
+ return -1;
23
+ }
24
+ function readFlagValue(parts, flag) {
25
+ const idx = parts.indexOf(flag);
26
+ if (idx === -1)
27
+ return undefined;
28
+ const value = parts[idx + 1];
29
+ if (!value || value.startsWith("-"))
30
+ return undefined;
31
+ return value;
32
+ }
33
+ function findPrompt(parts, startIndex) {
34
+ for (let i = startIndex; i < parts.length; i += 1) {
35
+ const part = parts[i];
36
+ if (!part)
37
+ continue;
38
+ if (part === "-p" || part === "--print") {
39
+ const next = parts[i + 1];
40
+ if (next && !next.startsWith("-"))
41
+ return next;
42
+ }
43
+ if (part.startsWith("-")) {
44
+ const skipFlags = new Set([
45
+ "--output-format",
46
+ "--input-format",
47
+ "--model",
48
+ "--max-turns",
49
+ "--max-budget-usd",
50
+ "--tools",
51
+ "--allowedTools",
52
+ "--disallowedTools",
53
+ "--resume",
54
+ "-r",
55
+ "--session-id",
56
+ "--continue",
57
+ "-c",
58
+ ]);
59
+ if (skipFlags.has(part)) {
60
+ i += 1;
61
+ }
62
+ continue;
63
+ }
64
+ return part;
65
+ }
66
+ return undefined;
67
+ }
68
+ export function parseClaudeCommand(command) {
69
+ const parts = splitArgs(command);
70
+ const claudeIndex = findClaudeIndex(parts);
71
+ if (claudeIndex === -1)
72
+ return null;
73
+ const hasPrint = parts.includes("-p") || parts.includes("--print");
74
+ const continued = parts.includes("--continue") || parts.includes("-c");
75
+ const resume = readFlagValue(parts, "--resume") || readFlagValue(parts, "-r");
76
+ const model = readFlagValue(parts, "--model");
77
+ const prompt = findPrompt(parts, claudeIndex + 1);
78
+ return {
79
+ kind: hasPrint ? "claude-cli" : "claude-tui",
80
+ prompt,
81
+ resume,
82
+ continued,
83
+ model,
84
+ print: hasPrint,
85
+ };
86
+ }
87
+ export function summarizeClaudeCommand(command) {
88
+ const info = parseClaudeCommand(command);
89
+ if (!info)
90
+ return null;
91
+ if (info.prompt) {
92
+ return { ...info, doing: `prompt: ${info.prompt}` };
93
+ }
94
+ if (info.resume) {
95
+ return { ...info, doing: `resume: ${info.resume}` };
96
+ }
97
+ if (info.continued) {
98
+ return { ...info, doing: "continue" };
99
+ }
100
+ if (info.print) {
101
+ return { ...info, doing: "claude print" };
102
+ }
103
+ return { ...info, doing: "claude" };
104
+ }
105
+ export function deriveClaudeState(input) {
106
+ const info = input.info ?? null;
107
+ const baseThreshold = input.cpuThreshold ??
108
+ Number(process.env.CONSENSUS_CLAUDE_CPU_ACTIVE || process.env.CONSENSUS_CPU_ACTIVE || 1);
109
+ const hasWork = !!info?.prompt || !!info?.resume || !!info?.continued;
110
+ const isTui = info?.kind === "claude-tui";
111
+ const effectiveThreshold = isTui && !hasWork ? baseThreshold * 3 : baseThreshold;
112
+ const result = deriveStateWithHold({
113
+ cpu: input.cpu,
114
+ hasError: false,
115
+ lastEventAt: undefined,
116
+ inFlight: hasWork,
117
+ previousActiveAt: input.previousActiveAt,
118
+ now: input.now,
119
+ cpuThreshold: effectiveThreshold,
120
+ });
121
+ if (!hasWork && input.cpu <= effectiveThreshold) {
122
+ return { state: "idle", lastActiveAt: undefined };
123
+ }
124
+ return result;
125
+ }
package/dist/cli.js CHANGED
@@ -27,6 +27,9 @@ function printHelp() {
27
27
  process.stdout.write(` --port <port> Port (default 8787)\n`);
28
28
  process.stdout.write(` --poll <ms> Poll interval in ms\n`);
29
29
  process.stdout.write(` --codex-home <path> Override CODEX_HOME\n`);
30
+ process.stdout.write(` --opencode-host <h> OpenCode host (default 127.0.0.1)\n`);
31
+ process.stdout.write(` --opencode-port <p> OpenCode port (default 4096)\n`);
32
+ process.stdout.write(` --no-opencode-autostart Disable OpenCode server autostart\n`);
30
33
  process.stdout.write(` --process-match <re> Regex for process matching\n`);
31
34
  process.stdout.write(` --no-redact Disable PII redaction\n`);
32
35
  process.stdout.write(` -h, --help Show help\n`);
@@ -40,6 +43,9 @@ const host = readArg("--host");
40
43
  const port = readArg("--port");
41
44
  const poll = readArg("--poll");
42
45
  const codexHome = readArg("--codex-home");
46
+ const opencodeHost = readArg("--opencode-host");
47
+ const opencodePort = readArg("--opencode-port");
48
+ const noOpenCodeAutostart = hasFlag("--no-opencode-autostart");
43
49
  const match = readArg("--process-match");
44
50
  const noRedact = hasFlag("--no-redact");
45
51
  if (host)
@@ -50,6 +56,12 @@ if (poll)
50
56
  env.CONSENSUS_POLL_MS = poll;
51
57
  if (codexHome)
52
58
  env.CONSENSUS_CODEX_HOME = codexHome;
59
+ if (opencodeHost)
60
+ env.CONSENSUS_OPENCODE_HOST = opencodeHost;
61
+ if (opencodePort)
62
+ env.CONSENSUS_OPENCODE_PORT = opencodePort;
63
+ if (noOpenCodeAutostart)
64
+ env.CONSENSUS_OPENCODE_AUTOSTART = "0";
53
65
  if (match)
54
66
  env.CONSENSUS_PROCESS_MATCH = match;
55
67
  if (noRedact)
package/dist/codexLogs.js CHANGED
@@ -266,6 +266,8 @@ export async function updateTail(sessionPath) {
266
266
  partial: "",
267
267
  events: [],
268
268
  };
269
+ const prevMtime = state.lastMtimeMs;
270
+ state.lastMtimeMs = stat.mtimeMs;
269
271
  if (stat.size < state.offset) {
270
272
  state.offset = 0;
271
273
  state.partial = "";
@@ -313,15 +315,15 @@ export async function updateTail(sessionPath) {
313
315
  state.partial = lines.pop() || "";
314
316
  const startRe = /(turn|item|response)\.started/i;
315
317
  const endRe = /(turn|item|response)\.(completed|failed|errored)/i;
316
- for (const line of lines) {
318
+ const processLine = (line) => {
317
319
  if (!line.trim())
318
- continue;
320
+ return false;
319
321
  let ev;
320
322
  try {
321
323
  ev = JSON.parse(line);
322
324
  }
323
325
  catch {
324
- continue;
326
+ return false;
325
327
  }
326
328
  const ts = getEventTimestamp(ev);
327
329
  const { summary, kind, isError, model, type } = summarizeEvent(ev);
@@ -357,20 +359,39 @@ export async function updateTail(sessionPath) {
357
359
  if (state.events.length > MAX_EVENTS) {
358
360
  state.events = state.events.slice(-MAX_EVENTS);
359
361
  }
362
+ return true;
360
363
  }
361
- else {
362
- state.lastEventAt = Math.max(state.lastEventAt || 0, ts);
363
- if (isError) {
364
- state.lastError = {
365
- ts,
366
- type: typeof type === "string" ? type : "event",
367
- summary: "error",
368
- isError,
369
- };
370
- }
364
+ state.lastEventAt = Math.max(state.lastEventAt || 0, ts);
365
+ if (isError) {
366
+ state.lastError = {
367
+ ts,
368
+ type: typeof type === "string" ? type : "event",
369
+ summary: "error",
370
+ isError,
371
+ };
372
+ return true;
373
+ }
374
+ return true;
375
+ };
376
+ let parsedAny = false;
377
+ for (const line of lines) {
378
+ if (processLine(line))
379
+ parsedAny = true;
380
+ }
381
+ const candidate = state.partial.trim();
382
+ if (candidate.startsWith("{") && candidate.endsWith("}")) {
383
+ if (processLine(candidate)) {
384
+ parsedAny = true;
385
+ state.partial = "";
371
386
  }
372
387
  }
373
388
  state.offset = stat.size;
389
+ if (!state.lastEventAt && typeof stat.mtimeMs === "number") {
390
+ state.lastEventAt = stat.mtimeMs;
391
+ }
392
+ else if (stat.size > prevOffset && prevMtime && prevMtime !== stat.mtimeMs) {
393
+ state.lastEventAt = Math.max(state.lastEventAt || 0, stat.mtimeMs);
394
+ }
374
395
  tailStates.set(sessionPath, state);
375
396
  return state;
376
397
  }
@@ -0,0 +1,84 @@
1
+ function shouldWarn(options) {
2
+ return options?.silent ? false : true;
3
+ }
4
+ export async function getOpenCodeSessions(host = "localhost", port = 4096, options) {
5
+ const controller = new AbortController();
6
+ const timeoutMs = options?.timeoutMs ?? 5000;
7
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
8
+ const warn = shouldWarn(options);
9
+ try {
10
+ const response = await fetch(`http://${host}:${port}/session`, {
11
+ headers: {
12
+ Accept: "application/json",
13
+ "User-Agent": "consensus-scanner",
14
+ },
15
+ signal: controller.signal,
16
+ });
17
+ clearTimeout(timeoutId);
18
+ if (!response.ok) {
19
+ if (warn) {
20
+ console.warn(`OpenCode API error: ${response.status} ${response.statusText}`);
21
+ }
22
+ return { ok: false, sessions: [], status: response.status, reachable: true };
23
+ }
24
+ const contentType = response.headers.get("content-type") || "";
25
+ if (!contentType.includes("json")) {
26
+ if (warn) {
27
+ console.warn(`OpenCode API non-JSON response (${contentType || "unknown"})`);
28
+ }
29
+ return { ok: false, sessions: [], status: response.status, reachable: true, error: "non_json" };
30
+ }
31
+ const payload = await response.json();
32
+ if (Array.isArray(payload))
33
+ return { ok: true, sessions: payload, reachable: true };
34
+ if (payload && typeof payload === "object" && Array.isArray(payload.sessions)) {
35
+ return { ok: true, sessions: payload.sessions, reachable: true };
36
+ }
37
+ if (payload && typeof payload === "object" && Array.isArray(payload.data)) {
38
+ return { ok: true, sessions: payload.data, reachable: true };
39
+ }
40
+ return { ok: true, sessions: [], reachable: true };
41
+ }
42
+ catch (error) {
43
+ clearTimeout(timeoutId);
44
+ if (warn) {
45
+ console.warn("Failed to fetch OpenCode sessions:", error);
46
+ }
47
+ const errorCode = typeof error?.cause?.code === "string"
48
+ ? error.cause.code
49
+ : typeof error?.code === "string"
50
+ ? error.code
51
+ : undefined;
52
+ return { ok: false, sessions: [], error: errorCode, reachable: false };
53
+ }
54
+ }
55
+ export async function getOpenCodeSession(sessionId, host = "localhost", port = 4096, options) {
56
+ const controller = new AbortController();
57
+ const timeoutMs = options?.timeoutMs ?? 5000;
58
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
59
+ const warn = shouldWarn(options);
60
+ try {
61
+ const response = await fetch(`http://${host}:${port}/session/${sessionId}`, {
62
+ headers: {
63
+ Accept: "application/json",
64
+ "User-Agent": "consensus-scanner",
65
+ },
66
+ signal: controller.signal,
67
+ });
68
+ clearTimeout(timeoutId);
69
+ if (!response.ok) {
70
+ if (warn) {
71
+ console.warn(`OpenCode API error for session ${sessionId}: ${response.status} ${response.statusText}`);
72
+ }
73
+ return null;
74
+ }
75
+ return await response.json();
76
+ }
77
+ catch (error) {
78
+ clearTimeout(timeoutId);
79
+ if (warn) {
80
+ console.warn(`Failed to fetch OpenCode session ${sessionId}:`, error);
81
+ }
82
+ return null;
83
+ }
84
+ }