@integsec/agentic-pentest-proxy 0.3.4 → 0.3.6

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.
Files changed (47) hide show
  1. package/README.md +2 -0
  2. package/cloudflare/wrangler.jsonc +1 -1
  3. package/dist/bin/integsec-agentic-pentest-bridge.js +2 -2
  4. package/dist/bin/integsec-agentic-pentest-gateway.js +30 -2
  5. package/dist/bin/integsec-agentic-pentest-gateway.js.map +1 -1
  6. package/dist/bin/integsec-agentic-pentest-tui.js +5 -0
  7. package/dist/bin/integsec-agentic-pentest-tui.js.map +1 -1
  8. package/dist/src/audit/local.js +2 -2
  9. package/dist/src/audit/local.js.map +1 -1
  10. package/dist/src/config.js +1 -1
  11. package/dist/src/gateway/admin.html +3 -3
  12. package/dist/src/gateway/docs-home.html +20 -0
  13. package/dist/src/gateway/docs.d.ts +3 -0
  14. package/dist/src/gateway/docs.d.ts.map +1 -0
  15. package/dist/src/gateway/docs.html +12 -0
  16. package/dist/src/gateway/docs.js +94 -0
  17. package/dist/src/gateway/docs.js.map +1 -0
  18. package/dist/src/gateway/local-paths.d.ts +7 -0
  19. package/dist/src/gateway/local-paths.d.ts.map +1 -0
  20. package/dist/src/gateway/local-paths.js +30 -0
  21. package/dist/src/gateway/local-paths.js.map +1 -0
  22. package/dist/src/gateway/runtime.js +2 -2
  23. package/dist/src/gateway/server.d.ts.map +1 -1
  24. package/dist/src/gateway/server.js +24 -0
  25. package/dist/src/gateway/server.js.map +1 -1
  26. package/dist/src/gateway/state.d.ts +1 -1
  27. package/dist/src/index.d.ts +1 -1
  28. package/dist/src/index.js +1 -1
  29. package/docs/configuration.md +70 -0
  30. package/docs/development.md +17 -0
  31. package/docs/images/tui-in-scope.png +0 -0
  32. package/docs/images/tui-in-scope.svg +24 -0
  33. package/docs/images/tui-out-of-scope.png +0 -0
  34. package/docs/images/tui-out-of-scope.svg +25 -0
  35. package/docs/images/tui-policy-blocks.png +0 -0
  36. package/docs/images/tui-policy-blocks.svg +24 -0
  37. package/docs/integrations.md +74 -0
  38. package/docs/managed-gateway.md +4 -0
  39. package/docs/release-readiness-0.2.6.md +27 -0
  40. package/docs/security.md +33 -0
  41. package/docs/superpowers/plans/2026-03-26-mcp-scope-proxy.md +3051 -0
  42. package/docs/superpowers/plans/2026-03-28-tui-monitor.md +863 -0
  43. package/docs/superpowers/plans/2026-03-29-manifest-builder.md +3429 -0
  44. package/docs/superpowers/specs/2026-03-28-tui-monitor-design.md +138 -0
  45. package/docs/superpowers/specs/2026-03-29-manifest-builder-design.md +248 -0
  46. package/docs/tui.md +27 -0
  47. package/package.json +4 -2
@@ -0,0 +1,863 @@
1
+ # TUI Monitor Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a standalone Ink-based terminal dashboard that monitors audit JSONL files in real time.
6
+
7
+ **Architecture:** A separate bin entry (`integsec-agentic-pentest-tui`) renders an Ink React app. A custom hook watches the audit directory for new JSONL lines, parses them into `AuditEntry` objects, and feeds them to a dashboard with three panels: engagement header, stats, and live decision feed.
8
+
9
+ **Tech Stack:** Ink 5, React 18, TypeScript TSX, fs.watch()
10
+
11
+ ---
12
+
13
+ ### Task 1: Install dependencies and configure TSX build
14
+
15
+ **Files:**
16
+ - Modify: `package.json`
17
+ - Modify: `tsconfig.json`
18
+
19
+ - [ ] **Step 1: Install ink, react, and types**
20
+
21
+ ```bash
22
+ npm install ink@^5.0.0 react@^18.0.0 ink-spinner@^5.0.0
23
+ npm install -D @types/react@^18.0.0
24
+ ```
25
+
26
+ - [ ] **Step 2: Add jsx support to tsconfig.json**
27
+
28
+ In `tsconfig.json`, add `"jsx": "react-jsx"` to `compilerOptions`:
29
+
30
+ ```json
31
+ {
32
+ "compilerOptions": {
33
+ "target": "ES2022",
34
+ "module": "Node16",
35
+ "moduleResolution": "Node16",
36
+ "lib": ["ES2022"],
37
+ "outDir": "./dist",
38
+ "rootDir": ".",
39
+ "strict": true,
40
+ "esModuleInterop": true,
41
+ "skipLibCheck": true,
42
+ "forceConsistentCasingInFileNames": true,
43
+ "resolveJsonModule": true,
44
+ "declaration": true,
45
+ "declarationMap": true,
46
+ "sourceMap": true,
47
+ "jsx": "react-jsx"
48
+ },
49
+ "include": ["src/**/*", "bin/**/*"],
50
+ "exclude": ["node_modules", "dist", "tests"]
51
+ }
52
+ ```
53
+
54
+ - [ ] **Step 3: Add bin entry to package.json**
55
+
56
+ Add to the `"bin"` section:
57
+
58
+ ```json
59
+ "bin": {
60
+ "integsec-agentic-pentest-proxy": "dist/bin/integsec-agentic-pentest-proxy.js",
61
+ "integsec-agentic-pentest-tui": "dist/bin/integsec-agentic-pentest-tui.js"
62
+ },
63
+ ```
64
+
65
+ Add `"src/tui/**/*"` is already covered by `"src/**/*"` in tsconfig include — no change needed.
66
+
67
+ - [ ] **Step 4: Verify build still works**
68
+
69
+ ```bash
70
+ npm run build
71
+ npm test
72
+ ```
73
+
74
+ Expected: All 598 tests pass, no TSX compilation errors (no TSX files yet, just verifying config doesn't break).
75
+
76
+ - [ ] **Step 5: Commit**
77
+
78
+ ```bash
79
+ git add package.json package-lock.json tsconfig.json
80
+ git commit -m "chore: add ink/react deps and jsx support for TUI"
81
+ ```
82
+
83
+ ---
84
+
85
+ ### Task 2: Build the JSONL tail/watcher utility
86
+
87
+ **Files:**
88
+ - Create: `src/tui/audit-tailer.ts`
89
+ - Test: `tests/unit/audit-tailer.test.ts`
90
+
91
+ - [ ] **Step 1: Write the failing test**
92
+
93
+ Create `tests/unit/audit-tailer.test.ts`:
94
+
95
+ ```typescript
96
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
97
+ import * as fs from "node:fs";
98
+ import * as path from "node:path";
99
+ import * as os from "node:os";
100
+ import { readNewEntries, scanExistingEntries } from "../src/tui/audit-tailer.js";
101
+ import type { AuditEntry } from "../src/types.js";
102
+
103
+ function makeEntry(overrides: Partial<AuditEntry> = {}): AuditEntry {
104
+ return {
105
+ timestamp: new Date().toISOString(),
106
+ engagement_id: "ENG-TEST-001",
107
+ client: "Test Client",
108
+ operator: "test@test.com",
109
+ tool_name: "nmap_scan",
110
+ tool_parameters: { target: "10.0.0.1" },
111
+ extracted_target: "10.0.0.1",
112
+ resolved_ips: ["10.0.0.1"],
113
+ decision: "ALLOWED",
114
+ decision_reason: "Target matches authorized IP range",
115
+ matched_scope_item: "10.0.0.0/24",
116
+ duration_ms: 2,
117
+ proxy_version: "0.1.2",
118
+ ...overrides,
119
+ };
120
+ }
121
+
122
+ describe("audit-tailer", () => {
123
+ let tmpDir: string;
124
+
125
+ beforeEach(() => {
126
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tui-test-"));
127
+ });
128
+
129
+ afterEach(() => {
130
+ fs.rmSync(tmpDir, { recursive: true, force: true });
131
+ });
132
+
133
+ describe("readNewEntries", () => {
134
+ it("reads entries from a given byte offset", () => {
135
+ const filePath = path.join(tmpDir, "ENG-TEST-001.jsonl");
136
+ const entry1 = makeEntry({ tool_name: "nmap_scan" });
137
+ const entry2 = makeEntry({ tool_name: "curl" });
138
+ fs.writeFileSync(filePath, JSON.stringify(entry1) + "\n" + JSON.stringify(entry2) + "\n");
139
+
140
+ const result = readNewEntries(filePath, 0);
141
+ expect(result.entries).toHaveLength(2);
142
+ expect(result.entries[0].tool_name).toBe("nmap_scan");
143
+ expect(result.entries[1].tool_name).toBe("curl");
144
+ expect(result.newOffset).toBeGreaterThan(0);
145
+ });
146
+
147
+ it("reads only new entries after offset", () => {
148
+ const filePath = path.join(tmpDir, "ENG-TEST-001.jsonl");
149
+ const entry1 = makeEntry({ tool_name: "nmap_scan" });
150
+ fs.writeFileSync(filePath, JSON.stringify(entry1) + "\n");
151
+ const firstRead = readNewEntries(filePath, 0);
152
+
153
+ const entry2 = makeEntry({ tool_name: "curl" });
154
+ fs.appendFileSync(filePath, JSON.stringify(entry2) + "\n");
155
+ const secondRead = readNewEntries(filePath, firstRead.newOffset);
156
+
157
+ expect(secondRead.entries).toHaveLength(1);
158
+ expect(secondRead.entries[0].tool_name).toBe("curl");
159
+ });
160
+
161
+ it("skips malformed lines", () => {
162
+ const filePath = path.join(tmpDir, "ENG-TEST-001.jsonl");
163
+ const entry = makeEntry();
164
+ fs.writeFileSync(filePath, "not json\n" + JSON.stringify(entry) + "\n" + "{bad\n");
165
+
166
+ const result = readNewEntries(filePath, 0);
167
+ expect(result.entries).toHaveLength(1);
168
+ expect(result.entries[0].tool_name).toBe("nmap_scan");
169
+ });
170
+
171
+ it("returns empty for nonexistent file", () => {
172
+ const result = readNewEntries(path.join(tmpDir, "nope.jsonl"), 0);
173
+ expect(result.entries).toHaveLength(0);
174
+ expect(result.newOffset).toBe(0);
175
+ });
176
+ });
177
+
178
+ describe("scanExistingEntries", () => {
179
+ it("reads all entries from all jsonl files in directory", () => {
180
+ const entry1 = makeEntry({ engagement_id: "ENG-001", tool_name: "nmap" });
181
+ const entry2 = makeEntry({ engagement_id: "ENG-002", tool_name: "curl" });
182
+ fs.writeFileSync(path.join(tmpDir, "ENG-001.jsonl"), JSON.stringify(entry1) + "\n");
183
+ fs.writeFileSync(path.join(tmpDir, "ENG-002.jsonl"), JSON.stringify(entry2) + "\n");
184
+
185
+ const result = scanExistingEntries(tmpDir);
186
+ expect(result.entries).toHaveLength(2);
187
+ expect(result.offsets.size).toBe(2);
188
+ });
189
+
190
+ it("filters by engagement ID when provided", () => {
191
+ const entry1 = makeEntry({ engagement_id: "ENG-001" });
192
+ const entry2 = makeEntry({ engagement_id: "ENG-002" });
193
+ fs.writeFileSync(path.join(tmpDir, "ENG-001.jsonl"), JSON.stringify(entry1) + "\n");
194
+ fs.writeFileSync(path.join(tmpDir, "ENG-002.jsonl"), JSON.stringify(entry2) + "\n");
195
+
196
+ const result = scanExistingEntries(tmpDir, "ENG-001");
197
+ expect(result.entries).toHaveLength(1);
198
+ expect(result.entries[0].engagement_id).toBe("ENG-001");
199
+ });
200
+
201
+ it("returns empty for empty directory", () => {
202
+ const result = scanExistingEntries(tmpDir);
203
+ expect(result.entries).toHaveLength(0);
204
+ expect(result.offsets.size).toBe(0);
205
+ });
206
+ });
207
+ });
208
+ ```
209
+
210
+ - [ ] **Step 2: Run test to verify it fails**
211
+
212
+ ```bash
213
+ npx vitest run tests/unit/audit-tailer.test.ts
214
+ ```
215
+
216
+ Expected: FAIL — cannot resolve `../src/tui/audit-tailer.js`
217
+
218
+ - [ ] **Step 3: Write the implementation**
219
+
220
+ Create `src/tui/audit-tailer.ts`:
221
+
222
+ ```typescript
223
+ import * as fs from "node:fs";
224
+ import * as path from "node:path";
225
+ import type { AuditEntry } from "../types.js";
226
+
227
+ export interface TailResult {
228
+ entries: AuditEntry[];
229
+ newOffset: number;
230
+ }
231
+
232
+ export interface ScanResult {
233
+ entries: AuditEntry[];
234
+ offsets: Map<string, number>;
235
+ }
236
+
237
+ /**
238
+ * Read new JSONL entries from a file starting at the given byte offset.
239
+ * Skips malformed lines. Returns parsed entries and the new offset.
240
+ */
241
+ export function readNewEntries(filePath: string, offset: number): TailResult {
242
+ if (!fs.existsSync(filePath)) {
243
+ return { entries: [], newOffset: offset };
244
+ }
245
+
246
+ const stat = fs.statSync(filePath);
247
+ if (stat.size <= offset) {
248
+ return { entries: [], newOffset: offset };
249
+ }
250
+
251
+ const fd = fs.openSync(filePath, "r");
252
+ const buf = Buffer.alloc(stat.size - offset);
253
+ fs.readSync(fd, buf, 0, buf.length, offset);
254
+ fs.closeSync(fd);
255
+
256
+ const text = buf.toString("utf-8");
257
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
258
+ const entries: AuditEntry[] = [];
259
+
260
+ for (const line of lines) {
261
+ try {
262
+ entries.push(JSON.parse(line) as AuditEntry);
263
+ } catch {
264
+ // Skip malformed lines
265
+ }
266
+ }
267
+
268
+ return { entries, newOffset: stat.size };
269
+ }
270
+
271
+ /**
272
+ * Scan all .jsonl files in the audit directory and return all entries
273
+ * plus the current byte offsets for each file (for subsequent tailing).
274
+ * Optionally filter to a single engagement ID.
275
+ */
276
+ export function scanExistingEntries(
277
+ auditDir: string,
278
+ engagementId?: string
279
+ ): ScanResult {
280
+ if (!fs.existsSync(auditDir)) {
281
+ return { entries: [], offsets: new Map() };
282
+ }
283
+
284
+ const files = fs.readdirSync(auditDir).filter((f) => f.endsWith(".jsonl"));
285
+ const allEntries: AuditEntry[] = [];
286
+ const offsets = new Map<string, number>();
287
+
288
+ for (const file of files) {
289
+ if (engagementId && file !== `${engagementId}.jsonl`) {
290
+ continue;
291
+ }
292
+ const filePath = path.join(auditDir, file);
293
+ const result = readNewEntries(filePath, 0);
294
+ allEntries.push(...result.entries);
295
+ offsets.set(filePath, result.newOffset);
296
+ }
297
+
298
+ allEntries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
299
+ return { entries: allEntries, offsets };
300
+ }
301
+ ```
302
+
303
+ - [ ] **Step 4: Run test to verify it passes**
304
+
305
+ ```bash
306
+ npm run build && npx vitest run tests/unit/audit-tailer.test.ts
307
+ ```
308
+
309
+ Expected: All 7 tests PASS.
310
+
311
+ - [ ] **Step 5: Commit**
312
+
313
+ ```bash
314
+ git add src/tui/audit-tailer.ts tests/unit/audit-tailer.test.ts
315
+ git commit -m "feat: add JSONL audit tailer for TUI monitor"
316
+ ```
317
+
318
+ ---
319
+
320
+ ### Task 3: Build the DecisionRow component
321
+
322
+ **Files:**
323
+ - Create: `src/tui/DecisionRow.tsx`
324
+
325
+ - [ ] **Step 1: Create DecisionRow component**
326
+
327
+ Create `src/tui/DecisionRow.tsx`:
328
+
329
+ ```tsx
330
+ import React from "react";
331
+ import { Text } from "ink";
332
+ import type { AuditEntry } from "../types.js";
333
+
334
+ interface DecisionRowProps {
335
+ entry: AuditEntry;
336
+ }
337
+
338
+ export function DecisionRow({ entry }: DecisionRowProps): React.ReactElement {
339
+ const time = entry.timestamp.slice(11, 19); // HH:MM:SS
340
+ const isBlocked = entry.decision.startsWith("BLOCKED");
341
+ const isNoTarget = entry.decision === "ALLOWED_NO_TARGET";
342
+
343
+ const icon = isBlocked ? "✗" : "✓";
344
+ const color = isBlocked ? "red" : isNoTarget ? "gray" : "green";
345
+
346
+ const shortDecision = entry.decision.replace("BLOCKED_", "");
347
+ const target = entry.extracted_target ?? "—";
348
+ const reason = isBlocked ? ` [${shortDecision}]` : "";
349
+
350
+ return (
351
+ <Text color={color}>
352
+ {time} {icon} {entry.decision.split("_")[0].padEnd(7)} {entry.tool_name} → {target}{reason} ({entry.duration_ms}ms)
353
+ </Text>
354
+ );
355
+ }
356
+ ```
357
+
358
+ - [ ] **Step 2: Verify build**
359
+
360
+ ```bash
361
+ npm run build
362
+ ```
363
+
364
+ Expected: Compiles without errors.
365
+
366
+ - [ ] **Step 3: Commit**
367
+
368
+ ```bash
369
+ git add src/tui/DecisionRow.tsx
370
+ git commit -m "feat: add DecisionRow TUI component"
371
+ ```
372
+
373
+ ---
374
+
375
+ ### Task 4: Build the EngagementHeader component
376
+
377
+ **Files:**
378
+ - Create: `src/tui/EngagementHeader.tsx`
379
+
380
+ - [ ] **Step 1: Create EngagementHeader component**
381
+
382
+ Create `src/tui/EngagementHeader.tsx`:
383
+
384
+ ```tsx
385
+ import React from "react";
386
+ import { Box, Text } from "ink";
387
+ import type { AuditEntry } from "../types.js";
388
+
389
+ interface EngagementHeaderProps {
390
+ entry: AuditEntry | null;
391
+ }
392
+
393
+ export function EngagementHeader({ entry }: EngagementHeaderProps): React.ReactElement {
394
+ if (!entry) {
395
+ return (
396
+ <Box borderStyle="single" paddingX={1}>
397
+ <Text dimColor>Waiting for audit data...</Text>
398
+ </Box>
399
+ );
400
+ }
401
+
402
+ return (
403
+ <Box borderStyle="single" paddingX={1} flexDirection="column">
404
+ <Text bold>
405
+ {entry.engagement_id} <Text dimColor>|</Text> {entry.client} <Text dimColor>|</Text> {entry.operator}
406
+ </Text>
407
+ </Box>
408
+ );
409
+ }
410
+ ```
411
+
412
+ - [ ] **Step 2: Verify build**
413
+
414
+ ```bash
415
+ npm run build
416
+ ```
417
+
418
+ Expected: Compiles without errors.
419
+
420
+ - [ ] **Step 3: Commit**
421
+
422
+ ```bash
423
+ git add src/tui/EngagementHeader.tsx
424
+ git commit -m "feat: add EngagementHeader TUI component"
425
+ ```
426
+
427
+ ---
428
+
429
+ ### Task 5: Build the StatsPanel component
430
+
431
+ **Files:**
432
+ - Create: `src/tui/StatsPanel.tsx`
433
+
434
+ - [ ] **Step 1: Create StatsPanel component**
435
+
436
+ Create `src/tui/StatsPanel.tsx`:
437
+
438
+ ```tsx
439
+ import React from "react";
440
+ import { Box, Text } from "ink";
441
+ import type { AuditEntry } from "../types.js";
442
+
443
+ interface StatsPanelProps {
444
+ entries: AuditEntry[];
445
+ }
446
+
447
+ function topN(counts: Map<string, number>, n: number): string {
448
+ return [...counts.entries()]
449
+ .sort((a, b) => b[1] - a[1])
450
+ .slice(0, n)
451
+ .map(([name, count]) => `${name} (${count})`)
452
+ .join(" ");
453
+ }
454
+
455
+ export function StatsPanel({ entries }: StatsPanelProps): React.ReactElement {
456
+ const allowed = entries.filter((e) => e.decision.startsWith("ALLOWED")).length;
457
+ const blocked = entries.length - allowed;
458
+
459
+ const targetCounts = new Map<string, number>();
460
+ const toolCounts = new Map<string, number>();
461
+
462
+ for (const e of entries) {
463
+ if (e.extracted_target) {
464
+ targetCounts.set(e.extracted_target, (targetCounts.get(e.extracted_target) ?? 0) + 1);
465
+ }
466
+ toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
467
+ }
468
+
469
+ return (
470
+ <Box borderStyle="single" paddingX={1} flexDirection="column">
471
+ <Text>
472
+ <Text color="green">✓ Allowed: {allowed}</Text>
473
+ {" "}
474
+ <Text color="red">✗ Blocked: {blocked}</Text>
475
+ {" "}
476
+ <Text>Total: {entries.length}</Text>
477
+ </Text>
478
+ {targetCounts.size > 0 && (
479
+ <Text dimColor>Targets: {topN(targetCounts, 3)}</Text>
480
+ )}
481
+ {toolCounts.size > 0 && (
482
+ <Text dimColor>Tools: {topN(toolCounts, 3)}</Text>
483
+ )}
484
+ </Box>
485
+ );
486
+ }
487
+ ```
488
+
489
+ - [ ] **Step 2: Verify build**
490
+
491
+ ```bash
492
+ npm run build
493
+ ```
494
+
495
+ Expected: Compiles without errors.
496
+
497
+ - [ ] **Step 3: Commit**
498
+
499
+ ```bash
500
+ git add src/tui/StatsPanel.tsx
501
+ git commit -m "feat: add StatsPanel TUI component"
502
+ ```
503
+
504
+ ---
505
+
506
+ ### Task 6: Build the LiveFeed component
507
+
508
+ **Files:**
509
+ - Create: `src/tui/LiveFeed.tsx`
510
+
511
+ - [ ] **Step 1: Create LiveFeed component**
512
+
513
+ Create `src/tui/LiveFeed.tsx`:
514
+
515
+ ```tsx
516
+ import React from "react";
517
+ import { Box, Text } from "ink";
518
+ import type { AuditEntry } from "../types.js";
519
+ import { DecisionRow } from "./DecisionRow.js";
520
+
521
+ interface LiveFeedProps {
522
+ entries: AuditEntry[];
523
+ maxRows: number;
524
+ }
525
+
526
+ export function LiveFeed({ entries, maxRows }: LiveFeedProps): React.ReactElement {
527
+ const visible = entries.slice(-maxRows);
528
+
529
+ if (visible.length === 0) {
530
+ return (
531
+ <Box borderStyle="single" paddingX={1} flexDirection="column">
532
+ <Text bold> LIVE FEED</Text>
533
+ <Text dimColor>No decisions yet...</Text>
534
+ </Box>
535
+ );
536
+ }
537
+
538
+ return (
539
+ <Box borderStyle="single" paddingX={1} flexDirection="column">
540
+ <Text bold> LIVE FEED</Text>
541
+ {visible.map((entry, i) => (
542
+ <DecisionRow key={`${entry.timestamp}-${i}`} entry={entry} />
543
+ ))}
544
+ </Box>
545
+ );
546
+ }
547
+ ```
548
+
549
+ - [ ] **Step 2: Verify build**
550
+
551
+ ```bash
552
+ npm run build
553
+ ```
554
+
555
+ Expected: Compiles without errors.
556
+
557
+ - [ ] **Step 3: Commit**
558
+
559
+ ```bash
560
+ git add src/tui/LiveFeed.tsx
561
+ git commit -m "feat: add LiveFeed TUI component"
562
+ ```
563
+
564
+ ---
565
+
566
+ ### Task 7: Build the App root component and useAuditWatcher hook
567
+
568
+ **Files:**
569
+ - Create: `src/tui/useAuditWatcher.ts`
570
+ - Create: `src/tui/App.tsx`
571
+
572
+ - [ ] **Step 1: Create the useAuditWatcher hook**
573
+
574
+ Create `src/tui/useAuditWatcher.ts`:
575
+
576
+ ```typescript
577
+ import { useState, useEffect, useRef } from "react";
578
+ import * as fs from "node:fs";
579
+ import * as path from "node:path";
580
+ import type { AuditEntry } from "../types.js";
581
+ import { readNewEntries, scanExistingEntries } from "./audit-tailer.js";
582
+
583
+ interface WatcherState {
584
+ entries: AuditEntry[];
585
+ }
586
+
587
+ export function useAuditWatcher(auditDir: string, engagementId?: string): WatcherState {
588
+ const [entries, setEntries] = useState<AuditEntry[]>([]);
589
+ const offsetsRef = useRef<Map<string, number>>(new Map());
590
+
591
+ useEffect(() => {
592
+ // Load existing entries
593
+ const scan = scanExistingEntries(auditDir, engagementId);
594
+ setEntries(scan.entries);
595
+ offsetsRef.current = scan.offsets;
596
+
597
+ // Ensure directory exists for watcher
598
+ if (!fs.existsSync(auditDir)) {
599
+ fs.mkdirSync(auditDir, { recursive: true });
600
+ }
601
+
602
+ // Watch for changes
603
+ const watcher = fs.watch(auditDir, (_event, filename) => {
604
+ if (!filename || !filename.endsWith(".jsonl")) return;
605
+ if (engagementId && filename !== `${engagementId}.jsonl`) return;
606
+
607
+ const filePath = path.join(auditDir, filename);
608
+ const currentOffset = offsetsRef.current.get(filePath) ?? 0;
609
+ const result = readNewEntries(filePath, currentOffset);
610
+
611
+ if (result.entries.length > 0) {
612
+ offsetsRef.current.set(filePath, result.newOffset);
613
+ setEntries((prev) => [...prev, ...result.entries]);
614
+ }
615
+ });
616
+
617
+ return () => {
618
+ watcher.close();
619
+ };
620
+ }, [auditDir, engagementId]);
621
+
622
+ return { entries };
623
+ }
624
+ ```
625
+
626
+ - [ ] **Step 2: Create the App root component**
627
+
628
+ Create `src/tui/App.tsx`:
629
+
630
+ ```tsx
631
+ import React from "react";
632
+ import { Box, useStdout } from "ink";
633
+ import { EngagementHeader } from "./EngagementHeader.js";
634
+ import { StatsPanel } from "./StatsPanel.js";
635
+ import { LiveFeed } from "./LiveFeed.js";
636
+ import { useAuditWatcher } from "./useAuditWatcher.js";
637
+
638
+ interface AppProps {
639
+ auditDir: string;
640
+ engagementId?: string;
641
+ }
642
+
643
+ export function App({ auditDir, engagementId }: AppProps): React.ReactElement {
644
+ const { entries } = useAuditWatcher(auditDir, engagementId);
645
+ const { stdout } = useStdout();
646
+ const termHeight = stdout?.rows ?? 24;
647
+
648
+ // Reserve rows for header (~3), stats (~4), feed border/title (~2), padding
649
+ const feedRows = Math.max(5, termHeight - 12);
650
+ const firstEntry = entries.length > 0 ? entries[0] : null;
651
+
652
+ return (
653
+ <Box flexDirection="column">
654
+ <EngagementHeader entry={firstEntry} />
655
+ <StatsPanel entries={entries} />
656
+ <LiveFeed entries={entries} maxRows={feedRows} />
657
+ </Box>
658
+ );
659
+ }
660
+ ```
661
+
662
+ - [ ] **Step 3: Verify build**
663
+
664
+ ```bash
665
+ npm run build
666
+ ```
667
+
668
+ Expected: Compiles without errors.
669
+
670
+ - [ ] **Step 4: Commit**
671
+
672
+ ```bash
673
+ git add src/tui/useAuditWatcher.ts src/tui/App.tsx
674
+ git commit -m "feat: add App root component and useAuditWatcher hook"
675
+ ```
676
+
677
+ ---
678
+
679
+ ### Task 8: Build the CLI entry point
680
+
681
+ **Files:**
682
+ - Create: `bin/integsec-agentic-pentest-tui.tsx`
683
+
684
+ - [ ] **Step 1: Create the entry point**
685
+
686
+ Create `bin/integsec-agentic-pentest-tui.tsx`:
687
+
688
+ ```tsx
689
+ #!/usr/bin/env node
690
+
691
+ import React from "react";
692
+ import { render } from "ink";
693
+ import { App } from "../src/tui/App.js";
694
+
695
+ function parseArgs(): { auditDir: string; engagementId?: string } {
696
+ const args = process.argv.slice(2);
697
+ let auditDir = process.env.AUDIT_LOG_PATH ?? "./audit";
698
+ let engagementId: string | undefined;
699
+
700
+ for (let i = 0; i < args.length; i++) {
701
+ if (args[i] === "--audit-path" && args[i + 1]) {
702
+ auditDir = args[i + 1];
703
+ i++;
704
+ } else if (args[i] === "--engagement" && args[i + 1]) {
705
+ engagementId = args[i + 1];
706
+ i++;
707
+ } else if (args[i] === "--help" || args[i] === "-h") {
708
+ console.log(`
709
+ IntegSec Agentic Pentest MCP Proxy — TUI Monitor
710
+
711
+ Usage:
712
+ integsec-agentic-pentest-tui [options]
713
+
714
+ Options:
715
+ --audit-path <dir> Path to audit log directory (default: AUDIT_LOG_PATH or ./audit)
716
+ --engagement <id> Filter to a specific engagement ID
717
+ -h, --help Show this help message
718
+ `);
719
+ process.exit(0);
720
+ }
721
+ }
722
+
723
+ return { auditDir, engagementId };
724
+ }
725
+
726
+ const { auditDir, engagementId } = parseArgs();
727
+ render(<App auditDir={auditDir} engagementId={engagementId} />);
728
+ ```
729
+
730
+ - [ ] **Step 2: Build and verify**
731
+
732
+ ```bash
733
+ npm run build
734
+ ```
735
+
736
+ Expected: Compiles, `dist/bin/integsec-agentic-pentest-tui.js` exists.
737
+
738
+ - [ ] **Step 3: Smoke test with empty audit directory**
739
+
740
+ ```bash
741
+ mkdir -p ./audit-test
742
+ node dist/bin/integsec-agentic-pentest-tui.js --audit-path ./audit-test
743
+ ```
744
+
745
+ Expected: Shows engagement header with "Waiting for audit data..." and empty live feed. Ctrl+C to exit.
746
+
747
+ ```bash
748
+ rm -rf ./audit-test
749
+ ```
750
+
751
+ - [ ] **Step 4: Commit**
752
+
753
+ ```bash
754
+ git add bin/integsec-agentic-pentest-tui.tsx
755
+ git commit -m "feat: add TUI monitor CLI entry point"
756
+ ```
757
+
758
+ ---
759
+
760
+ ### Task 9: Run full test suite and update README
761
+
762
+ **Files:**
763
+ - Modify: `README.md`
764
+ - Modify: `package.json` (files field)
765
+
766
+ - [ ] **Step 1: Run full test suite**
767
+
768
+ ```bash
769
+ npm run build && npm test
770
+ ```
771
+
772
+ Expected: All tests pass (existing 598 + 7 new audit-tailer tests = 605).
773
+
774
+ - [ ] **Step 2: Add TUI section to README**
775
+
776
+ In `README.md`, add a new section after the "Usage" section and before "How Validation Works". Add the following:
777
+
778
+ ```markdown
779
+ ## TUI Monitor
780
+
781
+ A real-time terminal dashboard for monitoring proxy decisions as they happen.
782
+
783
+ ```bash
784
+ # Monitor the default audit directory
785
+ integsec-agentic-pentest-tui
786
+
787
+ # Monitor a specific directory
788
+ integsec-agentic-pentest-tui --audit-path /path/to/audit
789
+
790
+ # Filter to a specific engagement
791
+ integsec-agentic-pentest-tui --engagement ENG-2026-TEST-001
792
+ ```
793
+
794
+ The TUI displays three panels:
795
+
796
+ - **Engagement Header** — engagement ID, client, operator
797
+ - **Stats Panel** — running allowed/blocked counts, top targets, top tools
798
+ - **Live Feed** — color-coded scrolling decision feed (green = allowed, red = blocked)
799
+
800
+ Run the TUI in a separate terminal while the proxy is active (via Claude Desktop, Claude Code, or CLI). It tails the audit JSONL files and updates in real time.
801
+ ```
802
+
803
+ - [ ] **Step 3: Add TUI to Table of Contents**
804
+
805
+ In the Table of Contents section, add after the Docker entry:
806
+
807
+ ```markdown
808
+ - [TUI Monitor](#tui-monitor)
809
+ ```
810
+
811
+ - [ ] **Step 4: Ensure TUI files are included in npm package**
812
+
813
+ The `"files"` field in `package.json` already includes `"dist/"` which covers the compiled TUI. No change needed.
814
+
815
+ - [ ] **Step 5: Commit**
816
+
817
+ ```bash
818
+ git add README.md
819
+ git commit -m "docs: add TUI monitor section to README"
820
+ ```
821
+
822
+ ---
823
+
824
+ ### Task 10: Bump version, publish, and push
825
+
826
+ **Files:**
827
+ - Modify: `package.json` (version)
828
+
829
+ - [ ] **Step 1: Bump version**
830
+
831
+ ```bash
832
+ npm version minor --no-git-tag-version
833
+ ```
834
+
835
+ This bumps to 0.2.0 since TUI is a new feature.
836
+
837
+ - [ ] **Step 2: Build and run full test suite one more time**
838
+
839
+ ```bash
840
+ npm run build && npm test
841
+ ```
842
+
843
+ Expected: All tests pass.
844
+
845
+ - [ ] **Step 3: Commit and push**
846
+
847
+ ```bash
848
+ git add -A
849
+ git commit -m "feat: add TUI monitor dashboard (v0.2.0)"
850
+ git push
851
+ ```
852
+
853
+ - [ ] **Step 4: Publish to npm**
854
+
855
+ ```bash
856
+ npm publish --access public
857
+ ```
858
+
859
+ - [ ] **Step 5: Update global install**
860
+
861
+ ```bash
862
+ npm install -g @integsec/agentic-pentest-proxy@latest
863
+ ```