@prads01/blackbox 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,48 +1,90 @@
1
1
  # BlackBox CLI
2
2
 
3
- Developer CLI for running BlackBox analysis against a local web app.
3
+ A CLI for real-time incident analysis, sandboxed fixes, and automated PR generation.
4
4
 
5
- ## Install (local dev)
5
+ ## What is BlackBox?
6
+ BlackBox is a local incident operations tool for engineering teams. It reads runtime evidence, identifies contradictions between expected and actual behavior, and produces structured analysis that can be verified in a safe sandbox. It is designed to shorten the path from detection to validated fix.
6
7
 
8
+ ## Overview
9
+ - Analyze local web apps via `/api/analyze`
10
+ - Detect and track incidents
11
+ - Generate sandboxed fixes
12
+ - Verify fixes before applying
13
+ - Create GitHub PRs directly
14
+ - Full TUI + CLI support
15
+
16
+ ## Features
17
+ - Detect runtime contradictions from logs
18
+ - AI-powered root cause analysis
19
+ - Sandbox fix verification (safe, isolated)
20
+ - One-click GitHub PR creation
21
+ - Interactive terminal UI (TUI)
22
+
23
+ ## Installation
7
24
  ```bash
8
- cd cli
9
- npm install
10
- npm run build
11
- npm link
25
+ npm install -g @prads01/blackbox
26
+ ```
27
+
28
+ ## Quick Start
29
+ ```bash
30
+ blackbox setup
31
+ blackbox
12
32
  ```
13
33
 
14
- Then run:
34
+ `blackbox setup` initializes and validates `.blackbox.yaml`.
35
+ `blackbox` launches the interactive TUI.
15
36
 
37
+ ## Usage
38
+
39
+ Run analysis:
16
40
  ```bash
17
- blackbox --help
41
+ blackbox analyze
18
42
  ```
19
43
 
20
- ## Commands
44
+ Watch logs:
45
+ ```bash
46
+ blackbox watch
47
+ ```
21
48
 
22
- - `blackbox` - launch full-screen BlackBox TUI mode
23
- - `blackbox ui` - launch full-screen BlackBox TUI mode
24
- - `blackbox setup` - interactive onboarding and `.blackbox.yaml` generation
25
- - `blackbox init` - quick default config file creation
26
- - `blackbox analyze` - run one analysis against `/api/analyze`
27
- - `blackbox watch` - monitor log file and re-run analysis on changes
28
- - `blackbox status` - show config and API reachability
49
+ Open TUI:
50
+ ```bash
51
+ blackbox
52
+ ```
29
53
 
30
- ## TUI shortcuts
54
+ TUI navigation basics:
55
+ - Arrow keys to move
56
+ - Enter to open/execute
57
+ - Backspace to return from detail views
31
58
 
32
- - `a` run analyze
33
- - `w` start/stop watch mode
34
- - `i` incidents view
35
- - `d` dashboard view
36
- - `s` sandbox view
37
- - `x` run sandbox fix for latest incident
38
- - `r` refresh status/incidents
39
- - `q` quit
59
+ Key shortcuts:
60
+ - `a` analyze
61
+ - `i` incidents
62
+ - `s` sandbox
63
+ - `x` run fix
64
+ - `c` create PR
65
+ - `q` quit
40
66
 
41
- ## Publish to npm later
67
+ ## Workflow
68
+ 1. BlackBox detects an incident.
69
+ 2. AI analyzes likely root cause.
70
+ 3. Sandbox verifies a fix safely.
71
+ 4. User creates a GitHub PR.
72
+ 5. Incident is marked resolved.
42
73
 
74
+ ## Configuration
75
+ BlackBox uses `.blackbox.yaml` for local runtime configuration. Core fields:
76
+ - project name
77
+ - API base URL
78
+ - log path
79
+
80
+ ## Development
43
81
  ```bash
82
+ git clone <repo>
44
83
  cd cli
84
+ npm install
45
85
  npm run build
46
- npm login
47
- npm publish --access public
86
+ npm link
48
87
  ```
88
+
89
+ ## License
90
+ MIT
@@ -1,10 +1,10 @@
1
1
  import { callAnalyzeApi } from "../api.js";
2
- import { readConfig } from "../config.js";
3
2
  import { loadEvidenceFromConfig } from "../files.js";
3
+ import { ensureRepoLinkedForCli } from "../linking.js";
4
4
  import { printTruthReport } from "../report.js";
5
5
  import { colors, section } from "../utils/format.js";
6
6
  export async function runAnalyzeOnce() {
7
- const config = await readConfig();
7
+ const config = await ensureRepoLinkedForCli();
8
8
  const evidence = await loadEvidenceFromConfig(config);
9
9
  const payload = {
10
10
  project: {
@@ -139,6 +139,7 @@ export async function runSetup() {
139
139
  expected_file: expectedFile,
140
140
  config_file: configFile,
141
141
  commit_file: base.commit_file || defaults.commit_file,
142
+ linked_repo: base.linked_repo,
142
143
  };
143
144
  const writeSpinner = ora("Writing .blackbox.yaml").start();
144
145
  const configPath = await writeConfig(configToWrite);
@@ -1,9 +1,9 @@
1
1
  import { isApiReachable } from "../api.js";
2
- import { readConfig } from "../config.js";
2
+ import { ensureRepoLinkedForCli } from "../linking.js";
3
3
  import { colors, row, section } from "../utils/format.js";
4
4
  export async function runStatus() {
5
5
  try {
6
- const config = await readConfig();
6
+ const config = await ensureRepoLinkedForCli();
7
7
  section("BlackBox Status");
8
8
  row("Project:", config.project_name);
9
9
  row("Environment:", config.environment);
@@ -12,6 +12,7 @@ export async function runStatus() {
12
12
  row("Expected file:", config.expected_file);
13
13
  row("Config file:", config.config_file);
14
14
  row("Commit file:", config.commit_file);
15
+ row("Linked repo:", config.linked_repo ?? colors.warn("not linked"));
15
16
  const reachable = await isApiReachable(config);
16
17
  row("API reachable:", reachable ? colors.ok("yes") : colors.bad("no"));
17
18
  }
@@ -1,13 +1,13 @@
1
1
  import chokidar from "chokidar";
2
2
  import path from "node:path";
3
- import { readConfig } from "../config.js";
3
+ import { ensureRepoLinkedForCli } from "../linking.js";
4
4
  import { runAnalyzeOnce } from "./analyze.js";
5
5
  import { colors, row, section } from "../utils/format.js";
6
6
  const WATCH_DEBOUNCE_MS = 1200;
7
7
  const WATCH_COOLDOWN_MS = 2000;
8
8
  export async function runWatch() {
9
9
  try {
10
- const config = await readConfig();
10
+ const config = await ensureRepoLinkedForCli();
11
11
  const absoluteLogPath = path.resolve(process.cwd(), config.log_file);
12
12
  let isAnalyzing = false;
13
13
  let debounceTimer = null;
package/dist/config.js CHANGED
@@ -10,10 +10,10 @@ export function createDefaultConfig(cwd = process.cwd()) {
10
10
  project_name: path.basename(cwd),
11
11
  environment: "local",
12
12
  api_base_url: "http://localhost:3000",
13
- log_file: "src/demo/cache-mismatch/logs.txt",
14
- expected_file: "src/demo/cache-mismatch/expected.md",
15
- config_file: "src/demo/cache-mismatch/config.json",
16
- commit_file: "src/demo/cache-mismatch/commit.diff",
13
+ log_file: "demo-system/logs/app.log",
14
+ expected_file: "demo-system/config/service-config.json",
15
+ config_file: "demo-system/state/system-state.json",
16
+ commit_file: "demo-system/config/recent-change.json",
17
17
  };
18
18
  }
19
19
  function assertConfigShape(value) {
@@ -0,0 +1,71 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readConfig, writeConfig } from "./config.js";
3
+ import { colors } from "./utils/format.js";
4
+ function openBrowser(url) {
5
+ const platform = process.platform;
6
+ if (platform === "darwin") {
7
+ const child = spawn("open", [url], { detached: true, stdio: "ignore" });
8
+ child.unref();
9
+ return;
10
+ }
11
+ if (platform === "win32") {
12
+ const child = spawn("cmd", ["/c", "start", "", url], {
13
+ detached: true,
14
+ stdio: "ignore",
15
+ });
16
+ child.unref();
17
+ return;
18
+ }
19
+ const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
20
+ child.unref();
21
+ }
22
+ async function startLinkFlow(config) {
23
+ const response = await fetch(`${config.api_base_url}/api/cli/link/start`, {
24
+ method: "POST",
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Unable to start CLI link flow (${response.status}).`);
28
+ }
29
+ return (await response.json());
30
+ }
31
+ async function getLinkStatus(config, token) {
32
+ const response = await fetch(`${config.api_base_url}/api/cli/link/${token}`, {
33
+ method: "GET",
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Unable to fetch CLI link status (${response.status}).`);
37
+ }
38
+ return (await response.json());
39
+ }
40
+ export async function ensureRepoLinkedForCli() {
41
+ const config = await readConfig();
42
+ if (config.linked_repo && config.linked_repo.trim().length > 0) {
43
+ return config;
44
+ }
45
+ console.log(colors.warn("No linked GitHub repo found for this CLI config."));
46
+ console.log(colors.muted("Starting browser-based auth and repo linking flow..."));
47
+ const start = await startLinkFlow(config);
48
+ console.log(colors.title(`Open this URL if browser does not open:\n${start.linkUrl}`));
49
+ try {
50
+ openBrowser(start.linkUrl);
51
+ }
52
+ catch {
53
+ // no-op, URL is already printed
54
+ }
55
+ const maxAttempts = 120;
56
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
57
+ await new Promise((resolve) => setTimeout(resolve, 2000));
58
+ const status = await getLinkStatus(config, start.token);
59
+ if (status.status !== "completed" || !status.repo)
60
+ continue;
61
+ const linkedRepo = status.repo.fullName;
62
+ const nextConfig = {
63
+ ...config,
64
+ linked_repo: linkedRepo,
65
+ };
66
+ await writeConfig(nextConfig);
67
+ console.log(colors.ok(`Linked repository: ${linkedRepo}`));
68
+ return nextConfig;
69
+ }
70
+ throw new Error("CLI link timed out. Complete browser auth and retry.");
71
+ }
package/dist/tui/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import blessed from "blessed";
2
- import { fetchApiReachability, fetchIncidents, loadTuiConfig, triggerCreatePr, triggerAnalysis, triggerSandboxFix, updateIncidentStatus, } from "./data.js";
2
+ import { fetchAnalysisRun, fetchApiReachability, fetchIncidents, loadTuiConfig, startAnalysisRun, triggerCreatePr, triggerSandboxFix, updateIncidentStatus, } from "./data.js";
3
3
  import { attachKeybindings } from "./keybindings.js";
4
4
  import { indexToView, palette, renderFooter, renderMain, renderSidebar, viewToIndex, } from "./render.js";
5
5
  import { TuiWatchService } from "./watch-service.js";
@@ -27,6 +27,7 @@ function createInitialState(config) {
27
27
  incidents: [],
28
28
  selectedIncidentIndex: 0,
29
29
  incidentsMode: "list",
30
+ contextSourceView: "logs",
30
31
  sandboxMode: "list",
31
32
  selectedSandboxIndex: 0,
32
33
  lastSandboxIncidentId: null,
@@ -39,6 +40,7 @@ function createInitialState(config) {
39
40
  sandboxPrError: null,
40
41
  sandboxPrInFlight: false,
41
42
  analyzeInFlight: false,
43
+ currentAnalysisRun: null,
42
44
  busyMessage: null,
43
45
  statusMessage: null,
44
46
  errorMessage: null,
@@ -220,12 +222,29 @@ export async function runTui() {
220
222
  state.analyzeInFlight = true;
221
223
  let didSucceed = false;
222
224
  await withBusy("Analyzing...", async () => {
223
- await triggerAnalysis(config);
224
- state.lastAnalysisAt = new Date().toISOString();
225
- await refreshData();
226
- state.view = "dashboard";
227
- state.selectedNavIndex = viewToIndex("dashboard");
228
- didSucceed = true;
225
+ const run = await startAnalysisRun(config);
226
+ state.currentAnalysisRun = run;
227
+ state.view = "analyze";
228
+ state.selectedNavIndex = viewToIndex("analyze");
229
+ render();
230
+ while (true) {
231
+ await new Promise((resolve) => setTimeout(resolve, 700));
232
+ const next = await fetchAnalysisRun(config, run.id);
233
+ state.currentAnalysisRun = next;
234
+ render();
235
+ if (next.status === "running")
236
+ continue;
237
+ if (next.status === "failed") {
238
+ state.errorMessage = next.error ?? "Analysis run failed.";
239
+ break;
240
+ }
241
+ didSucceed = true;
242
+ state.lastAnalysisAt = new Date().toISOString();
243
+ await refreshData();
244
+ state.view = "dashboard";
245
+ state.selectedNavIndex = viewToIndex("dashboard");
246
+ break;
247
+ }
229
248
  });
230
249
  state.analyzeInFlight = false;
231
250
  if (didSucceed) {
package/dist/tui/data.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isApiReachable } from "../api.js";
2
- import { readConfig } from "../config.js";
3
2
  import { loadEvidenceFromConfig } from "../files.js";
3
+ import { ensureRepoLinkedForCli } from "../linking.js";
4
4
  function assertIncidentsResponse(value) {
5
5
  if (!value || typeof value !== "object")
6
6
  return false;
@@ -25,11 +25,21 @@ function assertIncidentsResponse(value) {
25
25
  typeof i.impact === "string" &&
26
26
  typeof i.nextAction === "string" &&
27
27
  (i.evidencePoints === undefined || Array.isArray(i.evidencePoints)) &&
28
+ (i.investigation === undefined ||
29
+ (typeof i.investigation === "object" &&
30
+ i.investigation !== null &&
31
+ typeof i.investigation.investigationSummary ===
32
+ "string" &&
33
+ Array.isArray(i.investigation.evidence) &&
34
+ typeof i.investigation.confidence === "number")) &&
35
+ (i.investigationTimeline === undefined || Array.isArray(i.investigationTimeline)) &&
36
+ (i.investigationTools === undefined || Array.isArray(i.investigationTools)) &&
37
+ (i.toolOutputsSummary === undefined || Array.isArray(i.toolOutputsSummary)) &&
28
38
  typeof i.timestamp === "string");
29
39
  });
30
40
  }
31
41
  export async function loadTuiConfig() {
32
- return readConfig();
42
+ return ensureRepoLinkedForCli();
33
43
  }
34
44
  export async function fetchApiReachability(config) {
35
45
  return isApiReachable(config);
@@ -80,6 +90,69 @@ export async function triggerAnalysis(config) {
80
90
  }
81
91
  return result;
82
92
  }
93
+ function assertAnalysisRun(value) {
94
+ if (!value || typeof value !== "object")
95
+ return false;
96
+ const candidate = value;
97
+ if (typeof candidate.id !== "string" ||
98
+ (candidate.status !== "running" &&
99
+ candidate.status !== "completed" &&
100
+ candidate.status !== "failed") ||
101
+ typeof candidate.startedAt !== "string" ||
102
+ !Array.isArray(candidate.steps)) {
103
+ return false;
104
+ }
105
+ return candidate.steps.every((step) => {
106
+ if (!step || typeof step !== "object")
107
+ return false;
108
+ const s = step;
109
+ return (typeof s.id === "string" &&
110
+ typeof s.label === "string" &&
111
+ (s.status === "pending" || s.status === "running" || s.status === "completed"));
112
+ });
113
+ }
114
+ export async function startAnalysisRun(config) {
115
+ const evidence = await loadEvidenceFromConfig(config);
116
+ const payload = {
117
+ project: {
118
+ name: config.project_name,
119
+ environment: config.environment,
120
+ },
121
+ evidence,
122
+ };
123
+ const response = await fetch(`${config.api_base_url}/api/analyze/run`, {
124
+ method: "POST",
125
+ headers: {
126
+ "Content-Type": "application/json",
127
+ },
128
+ body: JSON.stringify(payload),
129
+ });
130
+ if (!response.ok) {
131
+ const body = await response.text();
132
+ throw new Error(`Analyze run start failed (${response.status}): ${body}`);
133
+ }
134
+ const payloadJson = (await response.json());
135
+ const run = payloadJson.run;
136
+ if (!assertAnalysisRun(run)) {
137
+ throw new Error("Analyze run start returned invalid payload.");
138
+ }
139
+ return run;
140
+ }
141
+ export async function fetchAnalysisRun(config, runId) {
142
+ const response = await fetch(`${config.api_base_url}/api/analyze/run/${runId}`, {
143
+ method: "GET",
144
+ });
145
+ if (!response.ok) {
146
+ const body = await response.text();
147
+ throw new Error(`Analyze run fetch failed (${response.status}): ${body}`);
148
+ }
149
+ const payload = (await response.json());
150
+ const run = payload.run;
151
+ if (!assertAnalysisRun(run)) {
152
+ throw new Error("Analyze run status payload invalid.");
153
+ }
154
+ return run;
155
+ }
83
156
  function assertSandboxResult(value) {
84
157
  if (!value || typeof value !== "object")
85
158
  return false;
@@ -56,6 +56,7 @@ export function attachKeybindings(screen, state, actions) {
56
56
  }
57
57
  if (state.view === "incidents" && state.incidentsMode === "list" && state.incidents.length > 0) {
58
58
  state.incidentsMode = "detail";
59
+ state.contextSourceView = "logs";
59
60
  actions.render();
60
61
  return;
61
62
  }
@@ -82,9 +83,34 @@ export function attachKeybindings(screen, state, actions) {
82
83
  screen.key(["i"], () => {
83
84
  state.view = "incidents";
84
85
  state.incidentsMode = "list";
86
+ state.contextSourceView = "logs";
85
87
  state.selectedNavIndex = 1;
86
88
  actions.render();
87
89
  });
90
+ screen.key(["1"], () => {
91
+ if (state.view === "incidents" && state.incidentsMode === "detail") {
92
+ state.contextSourceView = "logs";
93
+ actions.render();
94
+ }
95
+ });
96
+ screen.key(["2"], () => {
97
+ if (state.view === "incidents" && state.incidentsMode === "detail") {
98
+ state.contextSourceView = "config";
99
+ actions.render();
100
+ }
101
+ });
102
+ screen.key(["3"], () => {
103
+ if (state.view === "incidents" && state.incidentsMode === "detail") {
104
+ state.contextSourceView = "commits";
105
+ actions.render();
106
+ }
107
+ });
108
+ screen.key(["4"], () => {
109
+ if (state.view === "incidents" && state.incidentsMode === "detail") {
110
+ state.contextSourceView = "system_state";
111
+ actions.render();
112
+ }
113
+ });
88
114
  screen.key(["s"], () => {
89
115
  state.view = "sandbox";
90
116
  state.sandboxMode = "list";
@@ -108,6 +108,48 @@ function renderIncidentDetail(state) {
108
108
  if (!incident) {
109
109
  return "Incident not found.";
110
110
  }
111
+ const investigation = incident.investigation;
112
+ const evidence = investigation?.evidence && investigation.evidence.length > 0
113
+ ? investigation.evidence
114
+ : incident.evidencePoints ?? [];
115
+ const timeline = incident.investigationTimeline ?? [];
116
+ const investigationTools = incident.investigationTools ?? [];
117
+ const context = incident.context;
118
+ const contextHeader = [
119
+ "{bold}{white-fg}CONTEXT SOURCES{/white-fg}{/bold}",
120
+ `[1] Logs [2] Config [3] Commits [4] System State (active: ${state.contextSourceView})`,
121
+ ];
122
+ const contextBody = (() => {
123
+ if (!context)
124
+ return ["No context data available for this incident."];
125
+ if (state.contextSourceView === "logs") {
126
+ const logs = context.logs?.slice(-12) ?? [];
127
+ return logs.length > 0 ? logs : ["No log lines available."];
128
+ }
129
+ if (state.contextSourceView === "config") {
130
+ return [
131
+ `Service: ${context.systemState.service || "N/A"}`,
132
+ `Environment: ${context.systemState.environment || "N/A"}`,
133
+ `Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
134
+ ];
135
+ }
136
+ if (state.contextSourceView === "commits") {
137
+ const commits = context.recentCommits ?? [];
138
+ if (commits.length === 0)
139
+ return ["No commit context available."];
140
+ return commits.slice(0, 5).flatMap((commit) => [
141
+ `${commit.sha.slice(0, 7)} ${commit.message}`,
142
+ ` ${commit.summary}`,
143
+ ]);
144
+ }
145
+ return [
146
+ `Cache Version: ${context.systemState.cacheVersion || "N/A"}`,
147
+ `DB Version: ${context.systemState.dbVersion || "N/A"}`,
148
+ `Observed Topic: ${context.systemState.invalidationTopicObserved || "N/A"}`,
149
+ `Expected Topic: ${context.systemState.invalidationTopicExpected || "N/A"}`,
150
+ `Error Rate: ${context.systemState.recentErrorRate || "N/A"}`,
151
+ ];
152
+ })();
111
153
  return [
112
154
  "{bold}{white-fg}Incident Report{/white-fg}{/bold}",
113
155
  "{gray-fg}Backspace to return to incidents list.{/gray-fg}",
@@ -123,6 +165,10 @@ function renderIncidentDetail(state) {
123
165
  "{bold}{white-fg}Root Cause{/white-fg}{/bold}",
124
166
  incident.rootCause,
125
167
  "",
168
+ "{bold}{white-fg}Investigation Summary{/white-fg}{/bold}",
169
+ investigation?.investigationSummary ?? "No synthesized investigation summary available.",
170
+ `{bold}Confidence{/bold}: ${investigation ? `${Math.round(investigation.confidence * 100)}%` : "N/A"}`,
171
+ "",
126
172
  "{bold}{white-fg}Impact{/white-fg}{/bold}",
127
173
  incident.impact,
128
174
  "",
@@ -130,15 +176,37 @@ function renderIncidentDetail(state) {
130
176
  incident.fixSuggestion?.trim() || "No fix suggestion available.",
131
177
  "",
132
178
  "{bold}{white-fg}Evidence Points{/white-fg}{/bold}",
133
- ...(incident.evidencePoints?.length
134
- ? incident.evidencePoints.slice(0, 5).map((point) => `- ${point}`)
179
+ ...(evidence.length
180
+ ? evidence.slice(0, 5).map((point) => `- ${point}`)
135
181
  : ["- No evidence points available."]),
182
+ "",
183
+ "{bold}{white-fg}Investigation Timeline{/white-fg}{/bold}",
184
+ ...(timeline.length
185
+ ? timeline.slice(0, 8).map((entry) => {
186
+ const source = entry.source ? ` [${entry.source}]` : "";
187
+ return `${entry.step}. ${entry.title}${source}\n ${entry.detail}`;
188
+ })
189
+ : ["No timeline steps available."]),
190
+ "",
191
+ "{bold}{white-fg}Investigation Tools{/white-fg}{/bold}",
192
+ ...(investigationTools.length
193
+ ? investigationTools.map((tool) => {
194
+ const status = tool.status === "completed"
195
+ ? "{green-fg}completed{/green-fg}"
196
+ : "{red-fg}failed{/red-fg}";
197
+ return `- ${tool.name} [${status}]\n ${tool.summary}`;
198
+ })
199
+ : ["No tool execution records available."]),
200
+ "",
201
+ ...contextHeader,
202
+ ...contextBody,
136
203
  ].join("\n");
137
204
  }
138
205
  function renderIncidents(state) {
139
206
  return state.incidentsMode === "detail" ? renderIncidentDetail(state) : renderIncidentList(state);
140
207
  }
141
208
  function renderAnalyze(state) {
209
+ const run = state.currentAnalysisRun;
142
210
  return [
143
211
  "{bold}{white-fg}Analyze{/white-fg}{/bold}",
144
212
  "",
@@ -147,6 +215,29 @@ function renderAnalyze(state) {
147
215
  "",
148
216
  "Press {bold}a{/bold} to trigger analysis once.",
149
217
  "Repeated keypresses are ignored while analysis is running.",
218
+ "",
219
+ "{bold}{white-fg}Investigation Activity{/white-fg}{/bold}",
220
+ ...(run
221
+ ? [
222
+ `Run ID: ${run.id}`,
223
+ `Run Status: ${run.status === "completed"
224
+ ? "{green-fg}COMPLETED{/green-fg}"
225
+ : run.status === "failed"
226
+ ? "{red-fg}FAILED{/red-fg}"
227
+ : "{yellow-fg}RUNNING{/yellow-fg}"}`,
228
+ ...(run.incidentId ? [`Incident: ${run.incidentId}`] : []),
229
+ "",
230
+ ...run.steps.map((step, index) => {
231
+ const status = step.status === "completed"
232
+ ? "{green-fg}completed{/green-fg}"
233
+ : step.status === "running"
234
+ ? "{yellow-fg}running{/yellow-fg}"
235
+ : "{gray-fg}pending{/gray-fg}";
236
+ const detail = step.detail ? `\n ${step.detail}` : "";
237
+ return `${index + 1}. ${step.label} [${status}]${detail}`;
238
+ }),
239
+ ]
240
+ : ["No active investigation run."]),
150
241
  ].join("\n");
151
242
  }
152
243
  function renderWatch(state) {
@@ -269,9 +360,12 @@ export function renderMain(main, state) {
269
360
  }
270
361
  export function renderFooter(footer, state) {
271
362
  const hotkeysBase = "a analyze | w watch | i incidents | d dashboard | s sandbox | x sandbox-fix | f mark-fixed | v mark-investigating";
363
+ const contextHotkeys = state.view === "incidents" && state.incidentsMode === "detail"
364
+ ? " | 1 logs | 2 config | 3 commits | 4 system"
365
+ : "";
272
366
  const hotkeys = canCreatePrFromSandbox(state)
273
- ? `${hotkeysBase} | c create-pr | r refresh | q quit`
274
- : `${hotkeysBase} | r refresh | q quit`;
367
+ ? `${hotkeysBase}${contextHotkeys} | c create-pr | r refresh | q quit`
368
+ : `${hotkeysBase}${contextHotkeys} | r refresh | q quit`;
275
369
  const status = [
276
370
  `focus:${state.focusZone.toUpperCase()}`,
277
371
  `api:${state.apiReachable ? "up" : "down"}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prads01/blackbox",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "BlackBox CLI for local incident analysis and sandboxed fixes",
5
5
  "license": "MIT",
6
6
  "type": "module",