pentesting 0.16.3 → 0.16.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.
Files changed (2) hide show
  1. package/dist/main.js +1459 -1479
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -7,10 +7,11 @@ import chalk from "chalk";
7
7
  import gradient from "gradient-string";
8
8
 
9
9
  // src/platform/tui/app.tsx
10
+ import { useState as useState2, useCallback as useCallback2, useEffect as useEffect2 } from "react";
11
+ import { Box as Box5, useInput, useApp } from "ink";
12
+
13
+ // src/platform/tui/hooks/useAgent.ts
10
14
  import { useState, useEffect, useCallback, useRef } from "react";
11
- import { Box as Box2, Text as Text2, useInput, useApp, Static } from "ink";
12
- import TextInput from "ink-text-input";
13
- import Spinner from "ink-spinner";
14
15
 
15
16
  // src/shared/constants/agent.ts
16
17
  var AGENT_LIMITS = {
@@ -38,8 +39,22 @@ var DISPLAY_LIMITS = {
38
39
  var ID_LENGTH = 7;
39
40
  var ID_RADIX = 36;
40
41
  var APP_NAME = "Pentest AI";
41
- var APP_VERSION = "0.15.0";
42
+ var APP_VERSION = "0.16.3";
42
43
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
44
+ var UI_ROLES = {
45
+ SYSTEM: "system",
46
+ USER: "user",
47
+ AI: "ai",
48
+ ERROR: "error"
49
+ };
50
+ var LLM_ROLES = {
51
+ SYSTEM: "system",
52
+ USER: "user",
53
+ ASSISTANT: "assistant"
54
+ };
55
+
56
+ // src/shared/utils/id.ts
57
+ var generateId = (radix = 36, length = 8) => Math.random().toString(radix).substring(2, 2 + length);
43
58
 
44
59
  // src/shared/constants/protocol.ts
45
60
  var PHASES = {
@@ -59,7 +74,9 @@ var AGENT_ROLES = {
59
74
  WEB: "web",
60
75
  EXPLOTER: "exploit",
61
76
  DATABASE: "database",
62
- INFRA: "infra"
77
+ INFRA: "infra",
78
+ VULN: "vulnerability_analysis",
79
+ POST_EXPLOIT: "post_exploitation"
63
80
  };
64
81
  var SERVICES = {
65
82
  HTTP: "http",
@@ -166,11 +183,6 @@ var TODO_STATUSES = {
166
183
  DONE: "done",
167
184
  SKIPPED: "skipped"
168
185
  };
169
- var TODO_ACTIONS = {
170
- ADD: "add",
171
- COMPLETE: "complete",
172
- REMOVE: "remove"
173
- };
174
186
  var CORE_BINARIES = {
175
187
  NMAP: "nmap",
176
188
  MASSCAN: "masscan",
@@ -181,6 +193,12 @@ var CORE_BINARIES = {
181
193
  SQLMAP: "sqlmap",
182
194
  METASPLOIT: "msfconsole"
183
195
  };
196
+ var APPROVAL_STATUSES = {
197
+ AUTO: "auto",
198
+ USER_CONFIRMED: "user_confirmed",
199
+ USER_REVIEWED: "user_reviewed",
200
+ DENIED: "denied"
201
+ };
184
202
  var PRIORITIES = {
185
203
  HIGH: "high",
186
204
  MEDIUM: "medium",
@@ -191,6 +209,116 @@ var NOISE_LEVELS = {
191
209
  MEDIUM: "medium",
192
210
  HIGH: "high"
193
211
  };
212
+ var EVENT_TYPES = {
213
+ START: "start",
214
+ PHASE_CHANGE: "phase_change",
215
+ STATE_CHANGE: "state_change",
216
+ REASONING_START: "reasoning_start",
217
+ REASONING_DELTA: "reasoning_delta",
218
+ REASONING_END: "reasoning_end",
219
+ TOOL_CALL: "tool_call",
220
+ TOOL_RESULT: "tool_result",
221
+ ERROR: "error",
222
+ COMPLETE: "complete",
223
+ THINK: "think",
224
+ APPROVAL_ASK: "approval_ask",
225
+ APPROVAL_RESULT: "approval_result",
226
+ DELEGATE: "delegate",
227
+ DELEGATE_RESULT: "delegate_result",
228
+ ESCALATE: "escalate",
229
+ AGENT_SWITCH: "agent_switch",
230
+ OBSERVE: "observe",
231
+ RETRY: "retry",
232
+ USAGE_UPDATE: "usage_update",
233
+ INPUT_REQUEST: "input_request",
234
+ LOG: "log"
235
+ };
236
+ var UI_COMMANDS = {
237
+ HELP: "help",
238
+ HELP_SHORT: "h",
239
+ CLEAR: "clear",
240
+ CLEAR_SHORT: "c",
241
+ TARGET: "target",
242
+ TARGET_SHORT: "t",
243
+ START: "start",
244
+ START_SHORT: "s",
245
+ FINDINGS: "findings",
246
+ FINDINGS_SHORT: "f",
247
+ EXIT: "exit",
248
+ QUIT: "quit",
249
+ EXIT_SHORT: "q"
250
+ };
251
+ var PASSIVE_BINARIES = ["whois", "dig", "curl -i"];
252
+ var EXPLOIT_BINARIES = ["impacket"];
253
+ var MASK_PARTS = {
254
+ C24: "24",
255
+ C16: "16",
256
+ C8: "8"
257
+ };
258
+
259
+ // src/engine/state-serializer.ts
260
+ var StateSerializer = class {
261
+ /**
262
+ * Convert full state to a compact prompt summary
263
+ */
264
+ static toPrompt(state) {
265
+ const lines = [];
266
+ const data = state.data;
267
+ if (data.engagement) {
268
+ lines.push(`Engagement: ${data.engagement.name} (${data.engagement.client})`);
269
+ }
270
+ const scope = state.getScope();
271
+ if (scope) {
272
+ lines.push(`Scope: CIDR=[${scope.allowedCidrs.join(", ")}] Domains=[${scope.allowedDomains.join(", ")}]`);
273
+ }
274
+ const targets = state.getAllTargets();
275
+ if (targets.length > 0) {
276
+ const byCategory = /* @__PURE__ */ new Map();
277
+ for (const t of targets) {
278
+ const cat = t.primaryCategory || SERVICE_CATEGORIES.NETWORK;
279
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
280
+ byCategory.get(cat).push(t);
281
+ }
282
+ lines.push(`Targets (${targets.length}):`);
283
+ for (const [cat, catTargets] of byCategory) {
284
+ lines.push(` [${cat}] ${catTargets.length} hosts`);
285
+ for (const t of catTargets.slice(0, 3)) {
286
+ const ports = (t.ports || []).map((p) => `${p.port}/${p.service}`).join(", ");
287
+ lines.push(` ${t.ip}${t.hostname ? ` (${t.hostname})` : ""}: ${ports || "unknown"}`);
288
+ }
289
+ }
290
+ }
291
+ const findings = state.getFindings();
292
+ if (findings.length > 0) {
293
+ const counts = findings.reduce((acc, f) => {
294
+ acc[f.severity] = (acc[f.severity] || 0) + 1;
295
+ return acc;
296
+ }, { critical: 0, high: 0, medium: 0, low: 0 });
297
+ lines.push(`Findings: ${findings.length} total (crit:${counts.critical} high:${counts.high} med:${counts.medium})`);
298
+ const important = findings.filter((f) => f.severity === "critical" || f.severity === "high");
299
+ if (important.length > 0) {
300
+ lines.push(` Important Findings:`);
301
+ for (const f of important.slice(0, 5)) {
302
+ lines.push(` [${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})`);
303
+ }
304
+ }
305
+ }
306
+ const loot = state.getLoot();
307
+ if (loot.length > 0) {
308
+ lines.push(`Loot: ${loot.length} items`);
309
+ }
310
+ const todo = state.getTodo();
311
+ if (todo.length > 0) {
312
+ lines.push(`TODO (${todo.length}):`);
313
+ for (const t of todo.slice(0, DISPLAY_LIMITS.COMPACT_LIST_ITEMS)) {
314
+ const status = t.status === "done" ? "[x]" : t.status === "in_progress" ? "[->]" : "[ ]";
315
+ lines.push(` ${status} ${t.content} (${t.priority})`);
316
+ }
317
+ }
318
+ lines.push(`Phase: ${state.getPhase()}`);
319
+ return lines.join("\n");
320
+ }
321
+ };
194
322
 
195
323
  // src/engine/state.ts
196
324
  var SharedState = class {
@@ -206,14 +334,7 @@ var SharedState = class {
206
334
  currentPhase: PHASES.RECON
207
335
  };
208
336
  }
209
- // Engagement
210
- setEngagement(engagement) {
211
- this.data.engagement = engagement;
212
- }
213
- getEngagement() {
214
- return this.data.engagement;
215
- }
216
- // Scope
337
+ // --- Engagement & Scope ---
217
338
  getScope() {
218
339
  return this.data.engagement?.scope || null;
219
340
  }
@@ -222,16 +343,17 @@ var SharedState = class {
222
343
  this.data.engagement.scope = scope;
223
344
  } else {
224
345
  this.data.engagement = {
225
- id: Math.random().toString(36).slice(2, 10),
226
- name: "auto-engagement",
227
- client: "unknown",
346
+ id: generateId(ID_RADIX, ID_LENGTH),
347
+ name: "auto-assessment",
348
+ // Could be added to constants if needed
349
+ client: "internal",
228
350
  scope,
229
351
  phase: this.data.currentPhase,
230
352
  startedAt: Date.now()
231
353
  };
232
354
  }
233
355
  }
234
- // Targets
356
+ // --- Targets ---
235
357
  addTarget(target) {
236
358
  this.data.targets.set(target.ip, target);
237
359
  }
@@ -244,142 +366,49 @@ var SharedState = class {
244
366
  getTargets() {
245
367
  return this.data.targets;
246
368
  }
247
- /**
248
- * Update target with service fingerprint
249
- */
250
369
  addServiceFingerprint(ip, fingerprint) {
251
370
  const target = this.getTarget(ip);
252
- if (target) {
253
- if (!target.services) {
254
- target.services = [];
255
- }
256
- target.services.push(fingerprint);
257
- target.primaryCategory = this.getMostCommonCategory(target.services);
258
- }
259
- }
260
- /**
261
- * Get most common service category for target
262
- */
263
- getMostCommonCategory(services) {
264
- const counts = /* @__PURE__ */ new Map();
265
- for (const s of services) {
266
- counts.set(s.category, (counts.get(s.category) || 0) + 1);
267
- }
371
+ if (!target) return;
372
+ target.services = target.services || [];
373
+ target.services.push(fingerprint);
374
+ target.primaryCategory = this.calculatePrimaryCategory(target.services);
375
+ }
376
+ calculatePrimaryCategory(services) {
377
+ const counts = {};
378
+ let winner = SERVICE_CATEGORIES.NETWORK;
268
379
  let max = 0;
269
- let result = SERVICE_CATEGORIES.NETWORK;
270
- for (const [cat, count] of counts) {
271
- if (count > max) {
272
- max = count;
273
- result = cat;
380
+ for (const s of services) {
381
+ counts[s.category] = (counts[s.category] || 0) + 1;
382
+ if (counts[s.category] > max) {
383
+ max = counts[s.category];
384
+ winner = s.category;
274
385
  }
275
386
  }
276
- return result;
277
- }
278
- /**
279
- * Get targets by category
280
- */
281
- getTargetsByCategory(category) {
282
- return this.getAllTargets().filter(
283
- (t) => t.primaryCategory === category || t.services?.some((s) => s.category === category)
284
- );
387
+ return winner;
285
388
  }
286
- /**
287
- * Get high-risk targets
288
- */
289
- getHighRiskTargets() {
290
- const highRisk = [SERVICE_CATEGORIES.ICS, SERVICE_CATEGORIES.AD, SERVICE_CATEGORIES.DATABASE, SERVICE_CATEGORIES.CLOUD, SERVICE_CATEGORIES.CONTAINER];
291
- return this.getAllTargets().filter(
292
- (t) => highRisk.includes(t.primaryCategory || SERVICE_CATEGORIES.NETWORK)
293
- );
294
- }
295
- // Findings
389
+ // --- Findings & Loot ---
296
390
  addFinding(finding) {
297
391
  this.data.findings.push(finding);
298
392
  }
299
393
  getFindings() {
300
394
  return this.data.findings;
301
395
  }
302
- getFindingsBySeverity(severity) {
303
- return this.data.findings.filter((f) => f.severity === severity);
304
- }
305
- /**
306
- * Get findings by category
307
- */
308
- getFindingsByCategory(category) {
309
- return this.data.findings.filter((f) => f.category === category);
310
- }
311
- /**
312
- * Get findings by attack tactic
313
- */
314
- getFindingsByTactic(tactic) {
315
- return this.data.findings.filter((f) => f.attackPattern === tactic);
316
- }
317
- /**
318
- * Get verified findings only
319
- */
320
- getVerifiedFindings() {
321
- return this.data.findings.filter((f) => f.verified);
322
- }
323
- /**
324
- * Get findings with available exploits
325
- */
326
- getExploitableFindings() {
327
- return this.data.findings.filter((f) => f.exploitAvailable === true);
328
- }
329
- /**
330
- * Get chainable findings (attack chains)
331
- */
332
- getChainableFindings() {
333
- const chains = /* @__PURE__ */ new Map();
334
- for (const f of this.data.findings) {
335
- if (f.chainable && f.chainable.length > 0) {
336
- chains.set(f.id, [f, ...this.data.findings.filter((o) => f.chainable?.includes(o.id))]);
337
- }
338
- }
339
- return chains;
340
- }
341
- // Loot
342
396
  addLoot(loot) {
343
397
  this.data.loot.push(loot);
344
398
  }
345
399
  getLoot() {
346
400
  return this.data.loot;
347
401
  }
348
- /**
349
- * Get loot by category
350
- */
351
- getLootByCategory(category) {
352
- return this.data.loot.filter((l) => l.category === category);
353
- }
354
- /**
355
- * Get loot by type
356
- */
357
- getLootByType(type) {
358
- return this.data.loot.filter((l) => l.type === type);
359
- }
360
- /**
361
- * Get crackable loot (hashes, tickets)
362
- */
363
- getCrackableLoot() {
364
- return this.data.loot.filter((l) => l.crackable === true && !l.cracked);
365
- }
366
- // TODO List Management
367
- addTodo(content, priority = "medium") {
368
- const id = Math.random().toString(ID_RADIX).substring(ID_LENGTH);
369
- const todo = {
370
- id,
371
- content,
372
- status: TODO_STATUSES.PENDING,
373
- priority
374
- };
375
- this.data.todo.push(todo);
402
+ // --- TODO Management ---
403
+ addTodo(content, priority = PRIORITIES.MEDIUM) {
404
+ const id = generateId(ID_RADIX, ID_LENGTH);
405
+ const item = { id, content, status: TODO_STATUSES.PENDING, priority };
406
+ this.data.todo.push(item);
376
407
  return id;
377
408
  }
378
409
  updateTodo(id, updates) {
379
- const index = this.data.todo.findIndex((t) => t.id === id);
380
- if (index !== -1) {
381
- this.data.todo[index] = { ...this.data.todo[index], ...updates };
382
- }
410
+ const item = this.data.todo.find((t) => t.id === id);
411
+ if (item) Object.assign(item, updates);
383
412
  }
384
413
  getTodo() {
385
414
  return this.data.todo;
@@ -387,90 +416,29 @@ var SharedState = class {
387
416
  completeTodo(id) {
388
417
  this.updateTodo(id, { status: TODO_STATUSES.DONE });
389
418
  }
390
- // Action Log
419
+ // --- Logs & Phase ---
391
420
  logAction(action) {
392
- const log = {
393
- id: Math.random().toString(ID_RADIX).substring(ID_LENGTH),
421
+ this.data.actionLog.push({
422
+ id: generateId(ID_RADIX, ID_LENGTH),
394
423
  timestamp: Date.now(),
395
424
  ...action
396
- };
397
- this.data.actionLog.push(log);
425
+ });
398
426
  }
399
427
  getRecentActions(count = DISPLAY_LIMITS.COMPACT_LIST_ITEMS) {
400
428
  return this.data.actionLog.slice(-count);
401
429
  }
402
- getActionLog() {
403
- return this.data.actionLog;
404
- }
405
- // Phase
406
430
  setPhase(phase) {
407
431
  this.data.currentPhase = phase;
408
432
  }
409
433
  getPhase() {
410
434
  return this.data.currentPhase;
411
435
  }
412
- // Export to prompt (minimal summary)
436
+ /**
437
+ * @remarks
438
+ * WHY: Delegating complex string builder to specialized serializer.
439
+ */
413
440
  toPrompt() {
414
- const lines = [];
415
- if (this.data.engagement) {
416
- lines.push(`Engagement: ${this.data.engagement.name} (${this.data.engagement.client})`);
417
- }
418
- const scope = this.getScope();
419
- if (scope) {
420
- lines.push(`Scope: CIDR=[${scope.allowedCidrs.join(", ")}] Domains=[${scope.allowedDomains.join(", ")}]`);
421
- }
422
- const targets = this.getAllTargets();
423
- if (targets.length > 0) {
424
- const byCategory = /* @__PURE__ */ new Map();
425
- for (const t of targets) {
426
- const cat = t.primaryCategory || SERVICE_CATEGORIES.NETWORK;
427
- if (!byCategory.has(cat)) {
428
- byCategory.set(cat, []);
429
- }
430
- byCategory.get(cat).push(t);
431
- }
432
- lines.push(`Targets (${targets.length}):`);
433
- for (const [cat, catTargets] of byCategory) {
434
- lines.push(` [${cat}] ${catTargets.length} hosts`);
435
- for (const t of catTargets.slice(0, 3)) {
436
- const ports = t.ports.map((p) => `${p.port}/${p.service}`).join(", ");
437
- lines.push(` ${t.ip}${t.hostname ? ` (${t.hostname})` : ""}: ${ports || "unknown"}`);
438
- }
439
- }
440
- }
441
- const findings = this.getFindings();
442
- if (findings.length > 0) {
443
- const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
444
- for (const f of findings) {
445
- bySeverity[f.severity]++;
446
- }
447
- lines.push(`Findings: ${findings.length} total (crit:${bySeverity.critical} high:${bySeverity.high} medium:${bySeverity.medium})`);
448
- const criticalHigh = findings.filter((f) => f.severity === "critical" || f.severity === "high");
449
- if (criticalHigh.length > 0) {
450
- lines.push(` Critical/High:`);
451
- for (const f of criticalHigh.slice(0, 5)) {
452
- lines.push(` [${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})`);
453
- }
454
- }
455
- }
456
- const loot = this.getLoot();
457
- if (loot.length > 0) {
458
- const byType = /* @__PURE__ */ new Map();
459
- for (const l of loot) {
460
- byType.set(l.type, (byType.get(l.type) || 0) + 1);
461
- }
462
- lines.push(`Loot: ${loot.length} items (${Array.from(byType.entries()).map(([t, c]) => `${t}:${c}`).join(", ")})`);
463
- }
464
- const todo = this.getTodo();
465
- if (todo.length > 0) {
466
- lines.push(`TODO (${todo.length}):`);
467
- for (const t of todo.slice(0, DISPLAY_LIMITS.COMPACT_LIST_ITEMS)) {
468
- const status = t.status === "done" ? "[x]" : t.status === "in_progress" ? "[->]" : "[ ]";
469
- lines.push(` ${status} ${t.content} (${t.priority})`);
470
- }
471
- }
472
- lines.push(`Phase: ${this.getPhase()}`);
473
- return lines.join("\n");
441
+ return StateSerializer.toPrompt(this);
474
442
  }
475
443
  };
476
444
 
@@ -478,7 +446,11 @@ var SharedState = class {
478
446
  var AgentEventEmitter = class {
479
447
  listeners = /* @__PURE__ */ new Map();
480
448
  /**
481
- * Subscribe to events
449
+ * Subscribe to a specific event type with full type safety.
450
+ *
451
+ * @remarks
452
+ * WHY: Generic overload narrows the event type so subscribers get
453
+ * autocomplete on `event.data.*` without manual casting.
482
454
  */
483
455
  on(eventType, listener) {
484
456
  if (!this.listeners.has(eventType)) {
@@ -612,17 +584,17 @@ var ScopeGuard = class {
612
584
  if (target === cidr) return true;
613
585
  if (cidr.includes("/")) {
614
586
  const [network, mask] = cidr.split("/");
615
- if (mask === "24") {
587
+ if (mask === MASK_PARTS.C24) {
616
588
  const networkPrefix = network.split(".").slice(0, 3).join(".");
617
589
  const targetPrefix = target.split(".").slice(0, 3).join(".");
618
590
  return networkPrefix === targetPrefix;
619
591
  }
620
- if (mask === "8") {
592
+ if (mask === MASK_PARTS.C8) {
621
593
  const networkPrefix = network.split(".")[0];
622
594
  const targetPrefix = target.split(".")[0];
623
595
  return networkPrefix === targetPrefix;
624
596
  }
625
- if (mask === "16") {
597
+ if (mask === MASK_PARTS.C16) {
626
598
  const networkPrefix = network.split(".").slice(0, 2).join(".");
627
599
  const targetPrefix = target.split(".").slice(0, 2).join(".");
628
600
  return networkPrefix === targetPrefix;
@@ -668,7 +640,7 @@ function getApprovalLevel(toolCall) {
668
640
  const input = toolCall.input;
669
641
  if (tool === TOOL_NAMES.RUN_CMD) {
670
642
  const command = String(input.command || "").toLowerCase();
671
- if (["whois", "dig", "curl -i"].some((p) => command.includes(p))) {
643
+ if (PASSIVE_BINARIES.some((p) => command.includes(p))) {
672
644
  return APPROVAL_LEVELS.AUTO;
673
645
  }
674
646
  if ([CORE_BINARIES.NMAP, CORE_BINARIES.FFUF, CORE_BINARIES.NUCLEI].some((p) => command.includes(p))) {
@@ -677,7 +649,7 @@ function getApprovalLevel(toolCall) {
677
649
  }
678
650
  return APPROVAL_LEVELS.CONFIRM;
679
651
  }
680
- if ([CORE_BINARIES.SQLMAP, CORE_BINARIES.METASPLOIT, "impacket"].some((p) => command.includes(p))) {
652
+ if ([CORE_BINARIES.SQLMAP, CORE_BINARIES.METASPLOIT, ...EXPLOIT_BINARIES].some((p) => command.includes(p))) {
681
653
  return APPROVAL_LEVELS.REVIEW;
682
654
  }
683
655
  }
@@ -705,32 +677,83 @@ var ApprovalGate = class {
705
677
  };
706
678
 
707
679
  // src/engine/tools-base.ts
708
- import { execFileSync } from "child_process";
680
+ import { spawn } from "child_process";
709
681
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
710
682
  import { join } from "path";
711
- var DEFAULT_COMMAND_TIMEOUT = 3e4;
683
+ var DEFAULT_COMMAND_TIMEOUT = 3e5;
684
+ var INPUT_PROMPT_PATTERNS = [
685
+ /\[sudo\] password/i,
686
+ /Password:/i,
687
+ /password for/i,
688
+ /Enter passphrase/i,
689
+ /Are you sure.*\(yes\/no\)/i,
690
+ /\(y\/N\)/i,
691
+ /\(Y\/n\)/i
692
+ ];
693
+ var globalInputHandler = null;
694
+ function setInputHandler(handler) {
695
+ globalInputHandler = handler;
696
+ }
712
697
  async function runCommand(command, args = [], options = {}) {
713
- try {
714
- const parts = command.trim().split(/\s+/);
715
- const execPath = parts[0];
716
- const execArgs = [...parts.slice(1), ...args];
717
- const result = execFileSync(execPath, execArgs, {
718
- encoding: "utf-8",
719
- stdio: "pipe",
720
- timeout: DEFAULT_COMMAND_TIMEOUT,
721
- ...options
698
+ return new Promise((resolve) => {
699
+ const timeout = options.timeout || DEFAULT_COMMAND_TIMEOUT;
700
+ const child = spawn("sh", ["-c", command], {
701
+ timeout,
702
+ env: { ...process.env, ...options.env },
703
+ cwd: options.cwd
722
704
  });
723
- return {
724
- success: true,
725
- output: result.toString().trim()
726
- };
727
- } catch (error) {
728
- return {
729
- success: false,
730
- output: "",
731
- error: error.message || String(error)
705
+ let stdout = "";
706
+ let stderr = "";
707
+ let inputHandled = false;
708
+ const checkForInputPrompt = async (data) => {
709
+ if (inputHandled || !globalInputHandler) return;
710
+ for (const pattern of INPUT_PROMPT_PATTERNS) {
711
+ if (pattern.test(data)) {
712
+ inputHandled = true;
713
+ const promptText = data.trim();
714
+ try {
715
+ const userInput = await globalInputHandler(promptText);
716
+ if (userInput !== null && child.stdin.writable) {
717
+ child.stdin.write(userInput + "\n");
718
+ }
719
+ } catch {
720
+ }
721
+ return;
722
+ }
723
+ }
732
724
  };
733
- }
725
+ child.stdout.on("data", (data) => {
726
+ const text = data.toString();
727
+ stdout += text;
728
+ checkForInputPrompt(text);
729
+ });
730
+ child.stderr.on("data", (data) => {
731
+ const text = data.toString();
732
+ stderr += text;
733
+ checkForInputPrompt(text);
734
+ });
735
+ child.on("close", (code) => {
736
+ if (code === 0) {
737
+ resolve({
738
+ success: true,
739
+ output: stdout.trim() || stderr.trim()
740
+ });
741
+ } else {
742
+ resolve({
743
+ success: false,
744
+ output: stdout.trim(),
745
+ error: stderr.trim() || `Process exited with code ${code}`
746
+ });
747
+ }
748
+ });
749
+ child.on("error", (err) => {
750
+ resolve({
751
+ success: false,
752
+ output: "",
753
+ error: err.message || String(err)
754
+ });
755
+ });
756
+ });
734
757
  }
735
758
  async function readFileContent(filePath) {
736
759
  try {
@@ -774,8 +797,40 @@ async function writeFileContent(filePath, content) {
774
797
  }
775
798
  }
776
799
 
800
+ // src/engine/tools/system.ts
801
+ var systemTools = [
802
+ {
803
+ name: TOOL_NAMES.RUN_CMD,
804
+ description: "Execute shell command safely. Use for reconnaissance, scanning, and exploitation.",
805
+ parameters: {
806
+ command: { type: "string", description: "The shell command to execute" }
807
+ },
808
+ required: ["command"],
809
+ execute: async (params) => runCommand(params.command, [])
810
+ },
811
+ {
812
+ name: TOOL_NAMES.READ_FILE,
813
+ description: "Read local file content (logs, configs, evidence)",
814
+ parameters: {
815
+ path: { type: "string", description: "Path to the file" }
816
+ },
817
+ required: ["path"],
818
+ execute: async (params) => readFileContent(params.path)
819
+ },
820
+ {
821
+ name: TOOL_NAMES.WRITE_FILE,
822
+ description: "Write content to file (creates parent directories if needed)",
823
+ parameters: {
824
+ path: { type: "string", description: "Absolute path to the file" },
825
+ content: { type: "string", description: "File content" }
826
+ },
827
+ required: ["path", "content"],
828
+ execute: async (params) => writeFileContent(params.path, params.content)
829
+ }
830
+ ];
831
+
777
832
  // src/engine/tools-mid.ts
778
- import { execFileSync as execFileSync2 } from "child_process";
833
+ import { execFileSync } from "child_process";
779
834
  async function parseNmap(xmlPath) {
780
835
  try {
781
836
  const fileResult = await readFileContent(xmlPath);
@@ -843,7 +898,7 @@ async function searchExploitDB(service, version) {
843
898
  try {
844
899
  const query = version ? `${service} ${version}` : service;
845
900
  try {
846
- const output = execFileSync2("searchsploit", [query, "--color", "never"], {
901
+ const output = execFileSync("searchsploit", [query, "--color", "never"], {
847
902
  encoding: "utf-8",
848
903
  stdio: ["ignore", "pipe", "pipe"],
849
904
  timeout: 1e4
@@ -875,59 +930,93 @@ async function searchExploitDB(service, version) {
875
930
  };
876
931
  }
877
932
  }
878
- async function webSearch(query, engine = "duckduckgo") {
879
- try {
880
- let results = [];
881
- if (engine === "duckduckgo") {
882
- const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
883
- try {
884
- const html = execFileSync2("curl", ["-s", "-L", "-A", "Mozilla/5.0", url], {
885
- encoding: "utf-8",
886
- stdio: ["ignore", "pipe", "pipe"],
887
- timeout: 15e3
888
- });
889
- const titleRegex = /class="result__a"[^>]*>([^<]+)</g;
890
- const matches = html.match(titleRegex) || [];
891
- results = matches.map((m) => m.replace(/class="result__a"[^>]*>/, "")).slice(0, 10);
892
- } catch (e) {
893
- results = [`Web search failed: ${e.message}`];
894
- }
895
- } else {
896
- results = ["Search engine not supported. Use: duckduckgo"];
933
+
934
+ // src/engine/tools/pentest.ts
935
+ var createPentestTools = (state) => [
936
+ {
937
+ name: TOOL_NAMES.PARSE_NMAP,
938
+ description: "Parse nmap XML output to structured JSON",
939
+ parameters: { path: { type: "string", description: "Path to nmap XML" } },
940
+ required: ["path"],
941
+ execute: async (p) => parseNmap(p.path)
942
+ },
943
+ {
944
+ name: TOOL_NAMES.SEARCH_CVE,
945
+ description: "Search CVE and Exploit-DB for vulnerabilities",
946
+ parameters: {
947
+ service: { type: "string", description: "Service name" },
948
+ version: { type: "string", description: "Version number" }
949
+ },
950
+ required: ["service"],
951
+ execute: async (p) => searchCVE(p.service, p.version)
952
+ },
953
+ {
954
+ name: TOOL_NAMES.ADD_FINDING,
955
+ description: "Add a security finding",
956
+ parameters: {
957
+ title: { type: "string", description: "Finding title" },
958
+ severity: { type: "string", description: "Severity" },
959
+ affected: { type: "array", items: { type: "string" }, description: "Affected host:port" }
960
+ },
961
+ required: ["title", "severity"],
962
+ execute: async (p) => {
963
+ state.addFinding({
964
+ id: generateId(ID_RADIX, ID_LENGTH),
965
+ title: p.title,
966
+ severity: p.severity,
967
+ affected: p.affected || [],
968
+ description: p.description || "",
969
+ evidence: [],
970
+ verified: false,
971
+ remediation: "",
972
+ foundAt: Date.now()
973
+ });
974
+ return { success: true, output: `Added: ${p.title}` };
897
975
  }
898
- const formatted = results.map((r, i) => `${i + 1}. ${r}`).join("\n");
899
- return {
900
- success: true,
901
- output: formatted || `No results found for: ${query}`
902
- };
903
- } catch (error) {
904
- return {
905
- success: false,
906
- output: "",
907
- error: error.message || String(error)
908
- };
976
+ },
977
+ {
978
+ name: TOOL_NAMES.GET_STATE,
979
+ description: "Get current engagement state summary",
980
+ parameters: {},
981
+ execute: async () => ({ success: true, output: state.toPrompt() })
909
982
  }
910
- }
911
- async function extractURLs(text) {
912
- try {
913
- const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g;
914
- const urls = text.match(urlRegex) || [];
915
- const uniqueURLs = [...new Set(urls)];
916
- return {
983
+ ];
984
+
985
+ // src/engine/tools/agents.ts
986
+ var SUB_AGENT_PROMPTS = {
987
+ [AGENT_ROLES.RECON]: `You are a RECONNAISSANCE specialist. Focus on: OSINT, gathering information, and target discovery.`,
988
+ [AGENT_ROLES.WEB]: `You are a WEB APPLICATION specialist. Focus on: Endpoints, discovery, and web vulnerabilities.`,
989
+ [AGENT_ROLES.EXPLOTER]: `You are an EXPLOITATION specialist. Focus on: Exploit research and PoC development.`
990
+ };
991
+ var createAgentTools = (executor) => [
992
+ {
993
+ name: TOOL_NAMES.SPAWN_SUB,
994
+ description: "Spawn a sub-agent with specialized prompt",
995
+ parameters: {
996
+ task: { type: "string", description: "Specific task" },
997
+ agent_type: { type: "string", description: "Agent type (recon, web, exploit)" }
998
+ },
999
+ required: ["task"],
1000
+ execute: async (p) => {
1001
+ if (!executor) return { success: false, output: "SubAgentExecutor not available" };
1002
+ const type = p.agent_type || AGENT_ROLES.RECON;
1003
+ const prompt = SUB_AGENT_PROMPTS[type] || SUB_AGENT_PROMPTS[AGENT_ROLES.RECON];
1004
+ const result = await executor.executeSubTask(p.task, prompt);
1005
+ return { success: result.completed, output: result.output };
1006
+ }
1007
+ },
1008
+ {
1009
+ name: TOOL_NAMES.ASK_USER,
1010
+ description: "Ask the user a question and wait for input.",
1011
+ parameters: { question: { type: "string", description: "Question to ask" } },
1012
+ required: ["question"],
1013
+ execute: async (p) => ({
917
1014
  success: true,
918
- output: JSON.stringify({
919
- count: uniqueURLs.length,
920
- urls: uniqueURLs
921
- }, null, 2)
922
- };
923
- } catch (error) {
924
- return {
925
- success: false,
926
- output: "",
927
- error: error.message || String(error)
928
- };
1015
+ output: `[ASK_USER] ${p.question}
1016
+ (Waiting for user response via TUI input)`
1017
+ })
929
1018
  }
930
- }
1019
+ ];
931
1020
 
932
1021
  // src/engine/tools.ts
933
1022
  var ToolRegistry = class {
@@ -937,348 +1026,175 @@ var ToolRegistry = class {
937
1026
  this.approvalGate = approvalGate;
938
1027
  this.events = events;
939
1028
  this.subAgentExecutor = subAgentExecutor;
940
- this.registerLowLevelTools();
941
- this.registerMidLevelTools();
942
- this.registerHighLevelTools();
1029
+ this.initializeRegistry();
943
1030
  }
944
1031
  tools = /* @__PURE__ */ new Map();
945
- /**
946
- * Get all registered tools
947
- */
1032
+ initializeRegistry() {
1033
+ const allTools = [
1034
+ ...systemTools,
1035
+ ...createPentestTools(this.state),
1036
+ ...createAgentTools(this.subAgentExecutor)
1037
+ ];
1038
+ allTools.forEach((t) => this.tools.set(t.name, t));
1039
+ }
948
1040
  getAll() {
949
1041
  return Array.from(this.tools.values());
950
1042
  }
951
- /**
952
- * Get tool by name
953
- */
954
1043
  getTool(name) {
955
1044
  return this.tools.get(name);
956
1045
  }
957
1046
  /**
958
- * Execute tool with full pipeline
1047
+ * Execute tool with integrated safety pipeline (Scope -> Approval -> Execution -> Log)
1048
+ * Implements §8-1 (Safe Execution Pattern).
959
1049
  */
960
1050
  async execute(toolCall) {
961
1051
  const tool = this.getTool(toolCall.name);
962
- if (!tool) {
963
- return {
964
- success: false,
965
- output: "",
966
- error: `Unknown tool: ${toolCall.name}`
967
- };
968
- }
969
- const scopeResult = this.scopeGuard.check(toolCall);
970
- if (!scopeResult.allowed) {
971
- return {
972
- success: false,
973
- output: "",
974
- error: scopeResult.reason
975
- };
976
- }
1052
+ if (!tool) return { success: false, output: "", error: `Unknown tool: ${toolCall.name}` };
1053
+ const scopeCheck = this.scopeGuard.check(toolCall);
1054
+ if (!scopeCheck.allowed) return { success: false, output: "", error: scopeCheck.reason };
977
1055
  const approval = await this.approvalGate.request(toolCall);
978
1056
  if (!approval.approved) {
979
- this.state.logAction({
980
- tool: toolCall.name,
981
- command: JSON.stringify(toolCall.input),
982
- approval: "denied",
983
- noiseLevel: "none",
984
- outputSummary: approval.reason || "Denied"
985
- });
986
- return {
987
- success: false,
988
- output: "",
989
- error: approval.reason || "Execution denied"
990
- };
1057
+ this.logDeniedAction(toolCall, approval.reason || "Execution denied");
1058
+ return { success: false, output: "", error: approval.reason || "Denied by policy" };
991
1059
  }
992
1060
  const result = await tool.execute(toolCall.input);
1061
+ this.logSuccessfulAction(toolCall, approval, result);
1062
+ return result;
1063
+ }
1064
+ logDeniedAction(toolCall, reason) {
993
1065
  this.state.logAction({
994
1066
  tool: toolCall.name,
995
1067
  command: JSON.stringify(toolCall.input),
996
- approval: approval.approved ? "auto" : "user_confirmed",
997
- noiseLevel: getNoiseLevel(toolCall),
998
- outputSummary: result.output.slice(0, 200)
1068
+ approval: APPROVAL_STATUSES.DENIED,
1069
+ noiseLevel: NOISE_LEVELS.LOW,
1070
+ outputSummary: reason
999
1071
  });
1000
- return result;
1001
1072
  }
1002
- /**
1003
- * Register low-level tools
1004
- */
1005
- registerLowLevelTools() {
1006
- this.tools.set(TOOL_NAMES.RUN_CMD, {
1007
- name: TOOL_NAMES.RUN_CMD,
1008
- description: "Execute shell command safely. Use for reconnaissance, scanning, and exploitation.",
1009
- parameters: {
1010
- command: { type: "string", description: 'The shell command to execute (e.g., "nmap -sV 10.0.0.1")' }
1011
- },
1012
- required: ["command"],
1013
- execute: async (params) => {
1014
- const command = params.command;
1015
- return await runCommand(command, []);
1016
- }
1017
- });
1018
- this.tools.set(TOOL_NAMES.READ_FILE, {
1019
- name: TOOL_NAMES.READ_FILE,
1020
- description: "Read local file content (logs, configs, evidence)",
1021
- parameters: {
1022
- path: { type: "string", description: "Path to the file" }
1023
- },
1024
- required: ["path"],
1025
- execute: async (params) => {
1026
- const filePath = params.path;
1027
- return await readFileContent(filePath);
1028
- }
1029
- });
1030
- this.tools.set(TOOL_NAMES.WRITE_FILE, {
1031
- name: TOOL_NAMES.WRITE_FILE,
1032
- description: "Write content to file (creates parent directories if needed)",
1033
- parameters: {
1034
- path: { type: "string", description: "Absolute path to the file" },
1035
- content: { type: "string", description: "File content" }
1036
- },
1037
- required: ["path", "content"],
1038
- execute: async (params) => {
1039
- const filePath = params.path;
1040
- const content = params.content;
1041
- return await writeFileContent(filePath, content);
1042
- }
1073
+ logSuccessfulAction(toolCall, approval, result) {
1074
+ this.state.logAction({
1075
+ tool: toolCall.name,
1076
+ command: JSON.stringify(toolCall.input),
1077
+ approval: approval.approved ? APPROVAL_STATUSES.AUTO : APPROVAL_STATUSES.USER_CONFIRMED,
1078
+ noiseLevel: this.getNoiseLevel(toolCall),
1079
+ outputSummary: result.output.slice(0, 200)
1043
1080
  });
1044
1081
  }
1045
- /**
1046
- * Register mid-level tools
1047
- */
1048
- registerMidLevelTools() {
1049
- this.tools.set(TOOL_NAMES.PARSE_NMAP, {
1050
- name: TOOL_NAMES.PARSE_NMAP,
1051
- description: "Parse nmap XML output to structured JSON",
1052
- parameters: {
1053
- path: { type: "string", description: "Path to nmap XML output file" }
1054
- },
1055
- required: ["path"],
1056
- execute: async (params) => {
1057
- const xmlPath = params.path;
1058
- return await parseNmap(xmlPath);
1059
- }
1060
- });
1061
- this.tools.set(TOOL_NAMES.SEARCH_CVE, {
1062
- name: TOOL_NAMES.SEARCH_CVE,
1063
- description: "Search CVE and Exploit-DB for vulnerabilities",
1064
- parameters: {
1065
- service: { type: "string", description: 'Service name (e.g., "apache", "ssh")' },
1066
- version: { type: "string", description: "Version number (optional)" }
1067
- },
1068
- required: ["service"],
1069
- execute: async (params) => {
1070
- const service = params.service;
1071
- const version = params.version;
1072
- return await searchCVE(service, version);
1073
- }
1074
- });
1075
- this.tools.set(TOOL_NAMES.WEB_SEARCH, {
1076
- name: TOOL_NAMES.WEB_SEARCH,
1077
- description: "Search web for reconnaissance info",
1078
- parameters: {
1079
- query: { type: "string", description: "Search query" }
1080
- },
1081
- required: ["query"],
1082
- execute: async (params) => {
1083
- const query = params.query;
1084
- const engine = params.engine || "duckduckgo";
1085
- return await webSearch(query, engine);
1086
- }
1087
- });
1088
- this.tools.set(TOOL_NAMES.EXTRACT_URLS, {
1089
- name: TOOL_NAMES.EXTRACT_URLS,
1090
- description: "Extract and deduplicate URLs from text",
1091
- parameters: {
1092
- text: { type: "string", description: "Input text containing URLs" }
1093
- },
1094
- required: ["text"],
1095
- execute: async (params) => {
1096
- const text = params.text;
1097
- return await extractURLs(text);
1098
- }
1099
- });
1082
+ getNoiseLevel(toolCall) {
1083
+ if (toolCall.name !== TOOL_NAMES.RUN_CMD) return NOISE_LEVELS.LOW;
1084
+ const cmd = String(toolCall.input.command || "").toLowerCase();
1085
+ const highNoise = [CORE_BINARIES.NMAP, CORE_BINARIES.MASSCAN, CORE_BINARIES.NUCLEI, CORE_BINARIES.NIKTO];
1086
+ if (highNoise.some((b) => cmd.includes(b))) return NOISE_LEVELS.HIGH;
1087
+ const medNoise = [CORE_BINARIES.FFUF, CORE_BINARIES.GOBUSTER];
1088
+ if (medNoise.some((b) => cmd.includes(b))) return NOISE_LEVELS.MEDIUM;
1089
+ return NOISE_LEVELS.LOW;
1100
1090
  }
1101
- /**
1102
- * Register high-level tools
1103
- */
1104
- registerHighLevelTools() {
1105
- this.tools.set(TOOL_NAMES.SPAWN_SUB, {
1106
- name: TOOL_NAMES.SPAWN_SUB,
1107
- description: "Spawn a sub-agent with specialized prompt",
1108
- parameters: {
1109
- task: { type: "string", description: "Specific task for sub-agent" },
1110
- agent_type: { type: "string", description: "Sub-agent type (recon, web, exploit)" }
1111
- },
1112
- required: ["task"],
1113
- execute: async (params) => {
1114
- const task = params.task;
1115
- const agentType = params.agent_type || AGENT_ROLES.RECON;
1116
- if (!this.subAgentExecutor) {
1117
- return { success: false, output: "SubAgentExecutor not initialized in registry" };
1118
- }
1119
- const systemPrompt = this.getSpecializedPrompt(agentType, task);
1120
- const result = await this.subAgentExecutor.executeSubTask(task, systemPrompt);
1121
- return {
1122
- success: result.completed,
1123
- output: result.output
1124
- };
1125
- }
1126
- });
1127
- this.tools.set(TOOL_NAMES.ADD_FINDING, {
1128
- name: TOOL_NAMES.ADD_FINDING,
1129
- description: "Add a security finding",
1130
- parameters: {
1131
- title: { type: "string", description: "Finding title" },
1132
- severity: { type: "string", description: "Severity (info, low, medium, high, critical)" },
1133
- affected: { type: "array", items: { type: "string" }, description: "Affected host:port" },
1134
- description: { type: "string", description: "Finding detail" }
1135
- },
1136
- required: ["title", "severity"],
1137
- execute: async (params) => {
1138
- this.state.addFinding({
1139
- id: Math.random().toString(ID_RADIX).substring(ID_LENGTH),
1140
- title: params.title,
1141
- severity: params.severity,
1142
- affected: params.affected || [],
1143
- description: params.description || "",
1144
- evidence: [],
1145
- verified: false,
1146
- remediation: "",
1147
- foundAt: Date.now()
1148
- });
1149
- return {
1150
- success: true,
1151
- output: `Finding added: ${params.title}`
1152
- };
1153
- }
1154
- });
1155
- this.tools.set(TOOL_NAMES.UPDATE_TODO, {
1156
- name: TOOL_NAMES.UPDATE_TODO,
1157
- description: "Update plan / TODO list",
1158
- parameters: {
1159
- action: { type: "string", description: "add, complete" },
1160
- content: { type: "string", description: "Task content" }
1161
- },
1162
- required: ["action"],
1163
- execute: async (params) => {
1164
- if (params.action === TODO_ACTIONS.ADD) {
1165
- this.state.addTodo(params.content, params.priority);
1166
- } else if (params.action === TODO_ACTIONS.COMPLETE) {
1167
- this.state.completeTodo(params.id);
1168
- }
1169
- return {
1170
- success: true,
1171
- output: "TODO updated"
1172
- };
1173
- }
1174
- });
1175
- this.tools.set(TOOL_NAMES.GET_STATE, {
1176
- name: TOOL_NAMES.GET_STATE,
1177
- description: "Get current engagement state summary",
1178
- parameters: {},
1179
- execute: async (params) => {
1180
- return {
1181
- success: true,
1182
- output: this.state.toPrompt()
1183
- };
1184
- }
1185
- });
1186
- this.tools.set(TOOL_NAMES.SET_SCOPE, {
1187
- name: TOOL_NAMES.SET_SCOPE,
1188
- description: "Set engagement scope",
1189
- parameters: {
1190
- allowed: { type: "array", items: { type: "string" }, description: "Allowed CIDRs or domains" },
1191
- exclusions: { type: "array", items: { type: "string" }, description: "Excluded targets" }
1192
- },
1193
- required: ["allowed"],
1194
- execute: async (params) => {
1195
- const allowed = params.allowed || [];
1196
- const exclusions = params.exclusions || [];
1197
- this.state.setScope({
1198
- allowedCidrs: allowed.filter((a) => a.includes("/")),
1199
- allowedDomains: allowed.filter((a) => !a.includes("/")),
1200
- exclusions,
1201
- noDOS: true,
1202
- noSocial: true
1203
- });
1204
- return {
1205
- success: true,
1206
- output: `Scope set: ${allowed.length} targets`
1207
- };
1208
- }
1209
- });
1210
- this.tools.set(TOOL_NAMES.ADD_TARGET, {
1211
- name: TOOL_NAMES.ADD_TARGET,
1212
- description: "Add target to engagement",
1213
- parameters: {
1214
- ip: { type: "string", description: "IP address" },
1215
- hostname: { type: "string", description: "Hostname (optional)" }
1216
- },
1217
- required: ["ip"],
1218
- execute: async (params) => {
1219
- const ip = params.ip;
1220
- const hostname = params.hostname;
1221
- this.state.addTarget({
1222
- ip,
1223
- hostname,
1224
- ports: [],
1225
- tags: [],
1226
- firstSeen: Date.now()
1227
- });
1228
- return {
1229
- success: true,
1230
- output: `Target added: ${ip}`
1231
- };
1232
- }
1233
- });
1234
- this.tools.set(TOOL_NAMES.ASK_USER, {
1235
- name: TOOL_NAMES.ASK_USER,
1236
- description: "Ask the user a question and wait for input. Use when you need clarification, authorization, or additional information.",
1237
- parameters: {
1238
- question: { type: "string", description: "Question to ask" }
1239
- },
1240
- required: ["question"],
1241
- execute: async (params) => {
1242
- const question = params.question;
1243
- return {
1244
- success: true,
1245
- output: `[ASK_USER] ${question}
1246
- (Waiting for user response via TUI input)`
1247
- };
1248
- }
1249
- });
1091
+ };
1092
+
1093
+ // src/shared/constants/services.ts
1094
+ var PORT_CATEGORY_MAP = {
1095
+ 80: SERVICE_CATEGORIES.WEB,
1096
+ 443: SERVICE_CATEGORIES.WEB,
1097
+ 8080: SERVICE_CATEGORIES.WEB,
1098
+ 1433: SERVICE_CATEGORIES.DATABASE,
1099
+ 3306: SERVICE_CATEGORIES.DATABASE,
1100
+ 5432: SERVICE_CATEGORIES.DATABASE,
1101
+ 88: SERVICE_CATEGORIES.AD,
1102
+ 389: SERVICE_CATEGORIES.AD,
1103
+ 445: SERVICE_CATEGORIES.AD,
1104
+ 22: SERVICE_CATEGORIES.REMOTE_ACCESS,
1105
+ 3389: SERVICE_CATEGORIES.REMOTE_ACCESS,
1106
+ 21: SERVICE_CATEGORIES.FILE_SHARING,
1107
+ 2049: SERVICE_CATEGORIES.FILE_SHARING,
1108
+ 2375: SERVICE_CATEGORIES.CONTAINER,
1109
+ 5e3: SERVICE_CATEGORIES.CONTAINER,
1110
+ 502: SERVICE_CATEGORIES.ICS,
1111
+ 102: SERVICE_CATEGORIES.ICS
1112
+ };
1113
+ var SERVICE_CATEGORY_MAP = {
1114
+ [SERVICES.HTTP]: SERVICE_CATEGORIES.WEB,
1115
+ [SERVICES.HTTPS]: SERVICE_CATEGORIES.WEB,
1116
+ [SERVICES.MYSQL]: SERVICE_CATEGORIES.DATABASE,
1117
+ [SERVICES.MSSQL]: SERVICE_CATEGORIES.DATABASE,
1118
+ [SERVICES.SSH]: SERVICE_CATEGORIES.REMOTE_ACCESS,
1119
+ [SERVICES.FTP]: SERVICE_CATEGORIES.FILE_SHARING,
1120
+ "kerberos": SERVICE_CATEGORIES.AD,
1121
+ "ldap": SERVICE_CATEGORIES.AD,
1122
+ "docker": SERVICE_CATEGORIES.CONTAINER,
1123
+ "modbus": SERVICE_CATEGORIES.ICS
1124
+ };
1125
+ var CATEGORY_APPROVAL2 = {
1126
+ [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
1127
+ [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
1128
+ [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
1129
+ [SERVICE_CATEGORIES.AD]: APPROVAL_LEVELS.REVIEW,
1130
+ [SERVICE_CATEGORIES.EMAIL]: APPROVAL_LEVELS.CONFIRM,
1131
+ [SERVICE_CATEGORIES.REMOTE_ACCESS]: APPROVAL_LEVELS.REVIEW,
1132
+ [SERVICE_CATEGORIES.FILE_SHARING]: APPROVAL_LEVELS.CONFIRM,
1133
+ [SERVICE_CATEGORIES.CLOUD]: APPROVAL_LEVELS.REVIEW,
1134
+ [SERVICE_CATEGORIES.CONTAINER]: APPROVAL_LEVELS.REVIEW,
1135
+ [SERVICE_CATEGORIES.API]: APPROVAL_LEVELS.CONFIRM,
1136
+ [SERVICE_CATEGORIES.WIRELESS]: APPROVAL_LEVELS.REVIEW,
1137
+ [SERVICE_CATEGORIES.ICS]: APPROVAL_LEVELS.BLOCK
1138
+ };
1139
+ var CLOUD_KEYWORDS = [
1140
+ "amazonaws.com",
1141
+ "aws",
1142
+ " azure.com",
1143
+ "googleusercontent.com",
1144
+ "digitalocean.com",
1145
+ "heroku",
1146
+ "vercel"
1147
+ ];
1148
+ var PASSIVE_CATEGORIES = [
1149
+ SERVICE_CATEGORIES.NETWORK
1150
+ ];
1151
+ var ACTIVE_CATEGORIES = [
1152
+ SERVICE_CATEGORIES.WEB,
1153
+ SERVICE_CATEGORIES.API,
1154
+ SERVICE_CATEGORIES.EMAIL,
1155
+ SERVICE_CATEGORIES.FILE_SHARING
1156
+ ];
1157
+ var DANGER_LEVEL_MAP = {
1158
+ [SERVICE_CATEGORIES.NETWORK]: DANGER_LEVELS.PASSIVE,
1159
+ [SERVICE_CATEGORIES.WEB]: DANGER_LEVELS.ACTIVE,
1160
+ [SERVICE_CATEGORIES.API]: DANGER_LEVELS.ACTIVE,
1161
+ [SERVICE_CATEGORIES.EMAIL]: DANGER_LEVELS.ACTIVE,
1162
+ [SERVICE_CATEGORIES.REMOTE_ACCESS]: DANGER_LEVELS.EXPLOIT,
1163
+ [SERVICE_CATEGORIES.FILE_SHARING]: DANGER_LEVELS.ACTIVE,
1164
+ [SERVICE_CATEGORIES.DATABASE]: DANGER_LEVELS.EXPLOIT,
1165
+ [SERVICE_CATEGORIES.AD]: DANGER_LEVELS.EXPLOIT,
1166
+ [SERVICE_CATEGORIES.CLOUD]: DANGER_LEVELS.EXPLOIT,
1167
+ [SERVICE_CATEGORIES.CONTAINER]: DANGER_LEVELS.EXPLOIT,
1168
+ [SERVICE_CATEGORIES.WIRELESS]: DANGER_LEVELS.EXPLOIT,
1169
+ [SERVICE_CATEGORIES.ICS]: DANGER_LEVELS.EXPLOIT
1170
+ };
1171
+
1172
+ // src/engine/utils/service-parser.ts
1173
+ var ServiceParser = class {
1174
+ /** Detect category from port and service name */
1175
+ static detectCategory(port, serviceName) {
1176
+ if (PORT_CATEGORY_MAP[port]) return PORT_CATEGORY_MAP[port];
1177
+ if (serviceName && SERVICE_CATEGORY_MAP[serviceName.toLowerCase()]) {
1178
+ return SERVICE_CATEGORY_MAP[serviceName.toLowerCase()];
1179
+ }
1180
+ if (port >= 8e3 && port <= 9e3) return SERVICE_CATEGORIES.WEB;
1181
+ if (port >= 3e3 && port <= 3500) return SERVICE_CATEGORIES.API;
1182
+ return null;
1250
1183
  }
1251
- /**
1252
- * Get specialized prompt for sub-agent type
1253
- * Opus4.7: Agents are prompts, not code
1254
- */
1255
- getSpecializedPrompt(agentType, task) {
1256
- const prompts = {
1257
- [AGENT_ROLES.RECON]: `You are a RECONNAISSANCE specialist.
1258
- Your task: ${task}
1259
- Focus on: Information gathering, OSINT, passive reconnaissance, target discovery
1260
- Return: Structured target and port information for the main agent.`,
1261
- [AGENT_ROLES.WEB]: `You are a WEB APPLICATION specialist.
1262
- Your task: ${task}
1263
- Focus on: Web app discovery, enumeration, vulnerability scanning
1264
- Return: Web application structure, endpoints, vulnerabilities found.`,
1265
- [AGENT_ROLES.EXPLOTER]: `You are an EXPLOITATION specialist.
1266
- Your task: ${task}
1267
- Focus on: Exploit research, proof-of-concept testing
1268
- Return: Exploit results, shell access, credentials obtained.`
1269
- };
1270
- return prompts[agentType] || prompts[AGENT_ROLES.RECON];
1184
+ /** Parse category from banner string */
1185
+ static fromBanner(banner) {
1186
+ const b = banner.toLowerCase();
1187
+ if (CLOUD_KEYWORDS.some((k) => b.includes(k))) return SERVICE_CATEGORIES.CLOUD;
1188
+ if (["nginx", "apache", "iis"].some((k) => b.includes(k))) return SERVICE_CATEGORIES.WEB;
1189
+ if (["docker", "kubernetes", "kube"].some((k) => b.includes(k))) return SERVICE_CATEGORIES.CONTAINER;
1190
+ return null;
1271
1191
  }
1272
- };
1273
- function getNoiseLevel(toolCall) {
1274
- if (toolCall.name === TOOL_NAMES.RUN_CMD) {
1275
- const command = String(toolCall.input.command || "").toLowerCase();
1276
- if (command.includes(CORE_BINARIES.NMAP) || command.includes(CORE_BINARIES.MASSCAN)) return NOISE_LEVELS.HIGH;
1277
- if (command.includes(CORE_BINARIES.NUCLEI) || command.includes(CORE_BINARIES.NIKTO)) return NOISE_LEVELS.HIGH;
1278
- if (command.includes(CORE_BINARIES.FFUF) || command.includes(CORE_BINARIES.GOBUSTER)) return NOISE_LEVELS.MEDIUM;
1192
+ /** Check if hostname implies cloud infrastructure */
1193
+ static isCloudHostname(hostname) {
1194
+ const h = hostname.toLowerCase();
1195
+ return CLOUD_KEYWORDS.some((k) => h.includes(k));
1279
1196
  }
1280
- return NOISE_LEVELS.LOW;
1281
- }
1197
+ };
1282
1198
 
1283
1199
  // src/domains/registry.ts
1284
1200
  import { join as join2, dirname } from "path";
@@ -1360,181 +1276,12 @@ var DOMAINS = {
1360
1276
  };
1361
1277
 
1362
1278
  // src/engine/tools-registry.ts
1363
- var PORT_CATEGORY_MAP = {
1364
- // Web
1365
- 80: SERVICE_CATEGORIES.WEB,
1366
- 443: SERVICE_CATEGORIES.WEB,
1367
- 8080: SERVICE_CATEGORIES.WEB,
1368
- 8443: SERVICE_CATEGORIES.WEB,
1369
- 8e3: SERVICE_CATEGORIES.WEB,
1370
- 8888: SERVICE_CATEGORIES.WEB,
1371
- // Database
1372
- 1433: SERVICE_CATEGORIES.DATABASE,
1373
- // MSSQL
1374
- 3306: SERVICE_CATEGORIES.DATABASE,
1375
- // MySQL
1376
- 5432: SERVICE_CATEGORIES.DATABASE,
1377
- // PostgreSQL
1378
- 27017: SERVICE_CATEGORIES.DATABASE,
1379
- // MongoDB
1380
- 6379: SERVICE_CATEGORIES.DATABASE,
1381
- // Redis
1382
- 9042: SERVICE_CATEGORIES.DATABASE,
1383
- // Cassandra
1384
- 9200: SERVICE_CATEGORIES.DATABASE,
1385
- // Elasticsearch
1386
- // Active Directory
1387
- 88: SERVICE_CATEGORIES.AD,
1388
- // Kerberos
1389
- 389: SERVICE_CATEGORIES.AD,
1390
- // LDAP
1391
- 636: SERVICE_CATEGORIES.AD,
1392
- // LDAPS
1393
- 3268: SERVICE_CATEGORIES.AD,
1394
- // LDAP GC
1395
- 3269: SERVICE_CATEGORIES.AD,
1396
- // LDAP GC SSL
1397
- 445: SERVICE_CATEGORIES.AD,
1398
- // SMB (also file_sharing)
1399
- // Email
1400
- 25: SERVICE_CATEGORIES.EMAIL,
1401
- // SMTP
1402
- 587: SERVICE_CATEGORIES.EMAIL,
1403
- // SMTP Submission
1404
- 465: SERVICE_CATEGORIES.EMAIL,
1405
- // SMTPS
1406
- 110: SERVICE_CATEGORIES.EMAIL,
1407
- // POP3
1408
- 143: SERVICE_CATEGORIES.EMAIL,
1409
- // IMAP
1410
- 993: SERVICE_CATEGORIES.EMAIL,
1411
- // IMAPS
1412
- 995: SERVICE_CATEGORIES.EMAIL,
1413
- // POP3S
1414
- // Remote Access
1415
- 22: SERVICE_CATEGORIES.REMOTE_ACCESS,
1416
- // SSH
1417
- 3389: SERVICE_CATEGORIES.REMOTE_ACCESS,
1418
- // RDP
1419
- 5900: SERVICE_CATEGORIES.REMOTE_ACCESS,
1420
- // VNC
1421
- 5901: SERVICE_CATEGORIES.REMOTE_ACCESS,
1422
- // VNC
1423
- // File Sharing
1424
- 21: SERVICE_CATEGORIES.FILE_SHARING,
1425
- // FTP
1426
- 139: SERVICE_CATEGORIES.FILE_SHARING,
1427
- // NetBIOS
1428
- 2049: SERVICE_CATEGORIES.FILE_SHARING,
1429
- // NFS
1430
- 111: SERVICE_CATEGORIES.FILE_SHARING,
1431
- // RPC
1432
- // Container
1433
- 2375: SERVICE_CATEGORIES.CONTAINER,
1434
- // Docker API
1435
- 2376: SERVICE_CATEGORIES.CONTAINER,
1436
- // Docker API TLS
1437
- 5e3: SERVICE_CATEGORIES.CONTAINER,
1438
- // Docker Registry
1439
- // Cloud (detected via banner, common on 443)
1440
- // API
1441
- 3e3: SERVICE_CATEGORIES.API,
1442
- 5001: SERVICE_CATEGORIES.API,
1443
- 8001: SERVICE_CATEGORIES.API,
1444
- // Wireless
1445
- // ICS
1446
- 502: SERVICE_CATEGORIES.ICS,
1447
- // Modbus
1448
- 102: SERVICE_CATEGORIES.ICS,
1449
- // ISO-TSAP
1450
- 2e4: SERVICE_CATEGORIES.ICS,
1451
- // DNP3
1452
- 44818: SERVICE_CATEGORIES.ICS
1453
- // Ethernet/IP
1454
- };
1455
- var SERVICE_CATEGORY_MAP = {
1456
- // Web
1457
- [SERVICES.HTTP]: SERVICE_CATEGORIES.WEB,
1458
- [SERVICES.HTTPS]: SERVICE_CATEGORIES.WEB,
1459
- "http-proxy": SERVICE_CATEGORIES.WEB,
1460
- "ssl/http": SERVICE_CATEGORIES.WEB,
1461
- "nginx": SERVICE_CATEGORIES.WEB,
1462
- "apache": SERVICE_CATEGORIES.WEB,
1463
- "iis": SERVICE_CATEGORIES.WEB,
1464
- // Database
1465
- [SERVICES.MYSQL]: SERVICE_CATEGORIES.DATABASE,
1466
- [SERVICES.MSSQL]: SERVICE_CATEGORIES.DATABASE,
1467
- [SERVICES.POSTGRES]: SERVICE_CATEGORIES.DATABASE,
1468
- [SERVICES.MONGODB]: SERVICE_CATEGORIES.DATABASE,
1469
- [SERVICES.REDIS]: SERVICE_CATEGORIES.DATABASE,
1470
- [SERVICES.ELASTIC]: SERVICE_CATEGORIES.DATABASE,
1471
- "cassandra": SERVICE_CATEGORIES.DATABASE,
1472
- // Active Directory
1473
- "kerberos": SERVICE_CATEGORIES.AD,
1474
- "ldap": SERVICE_CATEGORIES.AD,
1475
- "ldaps": SERVICE_CATEGORIES.AD,
1476
- "microsoft-ds": SERVICE_CATEGORIES.AD,
1477
- "netbios-ssn": SERVICE_CATEGORIES.AD,
1478
- // Email
1479
- "smtp": SERVICE_CATEGORIES.EMAIL,
1480
- "pop3": SERVICE_CATEGORIES.EMAIL,
1481
- "imap": SERVICE_CATEGORIES.EMAIL,
1482
- // Remote Access
1483
- [SERVICES.SSH]: SERVICE_CATEGORIES.REMOTE_ACCESS,
1484
- "ms-wbt-server": SERVICE_CATEGORIES.REMOTE_ACCESS,
1485
- // RDP
1486
- "vnc": SERVICE_CATEGORIES.REMOTE_ACCESS,
1487
- // File Sharing
1488
- [SERVICES.FTP]: SERVICE_CATEGORIES.FILE_SHARING,
1489
- "ftp-data": SERVICE_CATEGORIES.FILE_SHARING,
1490
- "nfs": SERVICE_CATEGORIES.FILE_SHARING,
1491
- "rpcbind": SERVICE_CATEGORIES.FILE_SHARING,
1492
- // Container
1493
- "docker": SERVICE_CATEGORIES.CONTAINER,
1494
- "docker-swap": SERVICE_CATEGORIES.CONTAINER,
1495
- // Cloud APIs
1496
- "aws-s3": SERVICE_CATEGORIES.CLOUD,
1497
- "azure-storage": SERVICE_CATEGORIES.CLOUD,
1498
- // Wireless
1499
- // ICS
1500
- "modbus": SERVICE_CATEGORIES.ICS,
1501
- "iso-tsap": SERVICE_CATEGORIES.ICS,
1502
- "dnp3": SERVICE_CATEGORIES.ICS,
1503
- "enip": SERVICE_CATEGORIES.ICS
1504
- };
1505
- var CATEGORY_APPROVAL2 = {
1506
- [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
1507
- [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
1508
- [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
1509
- [SERVICE_CATEGORIES.AD]: APPROVAL_LEVELS.REVIEW,
1510
- [SERVICE_CATEGORIES.EMAIL]: APPROVAL_LEVELS.CONFIRM,
1511
- [SERVICE_CATEGORIES.REMOTE_ACCESS]: APPROVAL_LEVELS.REVIEW,
1512
- [SERVICE_CATEGORIES.FILE_SHARING]: APPROVAL_LEVELS.CONFIRM,
1513
- [SERVICE_CATEGORIES.CLOUD]: APPROVAL_LEVELS.REVIEW,
1514
- [SERVICE_CATEGORIES.CONTAINER]: APPROVAL_LEVELS.REVIEW,
1515
- [SERVICE_CATEGORIES.API]: APPROVAL_LEVELS.CONFIRM,
1516
- [SERVICE_CATEGORIES.WIRELESS]: APPROVAL_LEVELS.REVIEW,
1517
- [SERVICE_CATEGORIES.ICS]: APPROVAL_LEVELS.BLOCK
1518
- // Requires explicit written authorization
1519
- };
1520
- var CATEGORY_DESCRIPTIONS = Object.entries(DOMAINS).reduce((acc, [id, info]) => {
1521
- acc[id] = info.description;
1522
- return acc;
1523
- }, {});
1524
1279
  var CategorizedToolRegistry = class extends ToolRegistry {
1525
- categories;
1526
- constructor(state, scopeGuard, approvalGate, events, subAgentExecutor) {
1527
- super(state, scopeGuard, approvalGate, events, subAgentExecutor);
1528
- this.categories = /* @__PURE__ */ new Map();
1280
+ categories = /* @__PURE__ */ new Map();
1281
+ constructor(state, scopeGuard, approvalGate, events, executor) {
1282
+ super(state, scopeGuard, approvalGate, events, executor);
1529
1283
  this.initializeCategories();
1530
1284
  }
1531
- /**
1532
- * Initialize all tool categories with core tools from parent ToolRegistry
1533
- *
1534
- * Every service category gets the base tools (run_cmd, read_file, write_file, etc.)
1535
- * Sub-agents need at least these core tools to function.
1536
- * spawn_sub is explicitly excluded to prevent recursion (1-level only).
1537
- */
1538
1285
  initializeCategories() {
1539
1286
  const coreToolNames = [
1540
1287
  TOOL_NAMES.RUN_CMD,
@@ -1543,244 +1290,81 @@ var CategorizedToolRegistry = class extends ToolRegistry {
1543
1290
  TOOL_NAMES.PARSE_NMAP,
1544
1291
  TOOL_NAMES.SEARCH_CVE,
1545
1292
  TOOL_NAMES.WEB_SEARCH,
1546
- TOOL_NAMES.EXTRACT_URLS,
1547
1293
  TOOL_NAMES.ADD_FINDING,
1548
1294
  TOOL_NAMES.UPDATE_TODO,
1549
1295
  TOOL_NAMES.GET_STATE,
1550
1296
  TOOL_NAMES.ADD_TARGET,
1551
1297
  TOOL_NAMES.ASK_USER
1552
1298
  ];
1553
- const coreTools = [];
1554
- for (const name of coreToolNames) {
1555
- const tool = this.getTool(name);
1556
- if (tool) {
1557
- coreTools.push(tool);
1558
- }
1559
- }
1560
- for (const category of Object.keys(CATEGORY_DESCRIPTIONS)) {
1561
- this.categories.set(category, {
1562
- name: category,
1563
- description: CATEGORY_DESCRIPTIONS[category],
1299
+ const coreTools = coreToolNames.map((name) => this.getTool(name)).filter((t) => !!t);
1300
+ Object.keys(DOMAINS).forEach((id) => {
1301
+ const cat = id;
1302
+ this.categories.set(cat, {
1303
+ name: cat,
1304
+ description: DOMAINS[cat]?.description || "",
1564
1305
  tools: [...coreTools],
1565
- // Each category gets a copy of core tools
1566
- subAgentPrompt: "",
1567
- // Loaded from Domain Registry or prompt files at runtime
1568
- dangerLevel: this.getDangerLevel(category),
1569
- defaultApproval: CATEGORY_APPROVAL2[category],
1570
- commonPorts: this.getCommonPorts(category),
1571
- commonServices: this.getCommonServices(category)
1306
+ dangerLevel: this.calculateDanger(cat),
1307
+ defaultApproval: CATEGORY_APPROVAL2[cat] || "confirm"
1572
1308
  });
1573
- }
1574
- }
1575
- /**
1576
- * Get danger level for category
1577
- */
1578
- getDangerLevel(category) {
1579
- const passive = [SERVICE_CATEGORIES.NETWORK];
1580
- const active = [SERVICE_CATEGORIES.WEB, SERVICE_CATEGORIES.API, SERVICE_CATEGORIES.EMAIL, SERVICE_CATEGORIES.FILE_SHARING];
1581
- if (passive.includes(category)) return DANGER_LEVELS.PASSIVE;
1582
- if (active.includes(category)) return DANGER_LEVELS.ACTIVE;
1583
- return DANGER_LEVELS.EXPLOIT;
1584
- }
1585
- /**
1586
- * Get common ports for category
1587
- */
1588
- getCommonPorts(category) {
1589
- const ports = {
1590
- [SERVICE_CATEGORIES.NETWORK]: [1, 7, 9, 21, 22, 23, 25, 53, 79, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5432, 5900, 8080],
1591
- [SERVICE_CATEGORIES.WEB]: [80, 443, 8e3, 8080, 8443, 8888],
1592
- [SERVICE_CATEGORIES.DATABASE]: [1433, 3306, 5432, 6379, 9042, 9200, 27017, 1521],
1593
- [SERVICE_CATEGORIES.AD]: [88, 135, 139, 389, 445, 636, 3268, 3269, 5722],
1594
- [SERVICE_CATEGORIES.EMAIL]: [25, 110, 143, 465, 587, 993, 995],
1595
- [SERVICE_CATEGORIES.REMOTE_ACCESS]: [22, 23, 3389, 5900, 5901],
1596
- [SERVICE_CATEGORIES.FILE_SHARING]: [21, 139, 445, 2049, 111],
1597
- [SERVICE_CATEGORIES.CLOUD]: [443],
1598
- // Detected via banner
1599
- [SERVICE_CATEGORIES.CONTAINER]: [2375, 2376, 5e3],
1600
- [SERVICE_CATEGORIES.API]: [3e3, 5e3, 5001, 8e3, 8001, 8080],
1601
- [SERVICE_CATEGORIES.WIRELESS]: [],
1602
- [SERVICE_CATEGORIES.ICS]: [102, 502, 44818, 2e4]
1603
- };
1604
- return ports[category] || [];
1605
- }
1606
- /**
1607
- * Get common services for category
1608
- */
1609
- getCommonServices(category) {
1610
- const services = {
1611
- [SERVICE_CATEGORIES.NETWORK]: [SERVICES.FTP, SERVICES.SSH, "telnet", "smtp", SERVICES.HTTP, "pop3", "imap", SERVICES.HTTPS],
1612
- [SERVICE_CATEGORIES.WEB]: [SERVICES.HTTP, SERVICES.HTTPS, "nginx", "apache", "iis"],
1613
- [SERVICE_CATEGORIES.DATABASE]: [SERVICES.MYSQL, SERVICES.MSSQL, SERVICES.POSTGRES, SERVICES.MONGODB, SERVICES.REDIS, SERVICES.ELASTIC],
1614
- [SERVICE_CATEGORIES.AD]: ["kerberos", "ldap", "microsoft-ds", "netbios-ssn"],
1615
- [SERVICE_CATEGORIES.EMAIL]: ["smtp", "pop3", "imap"],
1616
- [SERVICE_CATEGORIES.REMOTE_ACCESS]: [SERVICES.SSH, "ms-wbt-server", "vnc"],
1617
- [SERVICE_CATEGORIES.FILE_SHARING]: [SERVICES.FTP, "microsoft-ds", "nfs"],
1618
- [SERVICE_CATEGORIES.CLOUD]: [SERVICES.HTTP, SERVICES.HTTPS],
1619
- // Detected via banner analysis
1620
- [SERVICE_CATEGORIES.CONTAINER]: ["docker"],
1621
- [SERVICE_CATEGORIES.API]: [SERVICES.HTTP, SERVICES.HTTPS],
1622
- [SERVICE_CATEGORIES.WIRELESS]: [],
1623
- [SERVICE_CATEGORIES.ICS]: ["modbus", "iso-tsap", "dnp3", "enip"]
1624
- };
1625
- return services[category] || [];
1626
- }
1627
- /**
1628
- * Get all tools in a category
1629
- */
1630
- getByCategory(category) {
1631
- return this.categories.get(category)?.tools || [];
1632
- }
1633
- /**
1634
- * Get category definition
1635
- */
1636
- getCategory(category) {
1637
- return this.categories.get(category);
1309
+ });
1638
1310
  }
1639
- /**
1640
- * Get all categories
1641
- */
1642
- getAllCategories() {
1643
- return Array.from(this.categories.values());
1311
+ calculateDanger(cat) {
1312
+ return DANGER_LEVEL_MAP[cat] || DANGER_LEVELS.EXPLOIT;
1644
1313
  }
1645
1314
  /**
1646
- * Suggest tools based on discovered target
1315
+ * Suggest tools based on target services
1647
1316
  */
1648
1317
  suggestTools(target) {
1649
- const suggestions = [];
1650
- for (const port of target.ports) {
1651
- const category = this.detectCategoryFromPort(port.port, port.service);
1652
- if (category && !suggestions.find((s) => s.category === category)) {
1653
- const catTools = this.getByCategory(category);
1654
- suggestions.push({
1655
- category,
1656
- tools: catTools.map((t) => t.name)
1657
- });
1658
- }
1659
- }
1660
- if (target.hostname) {
1661
- const cloudCat = this.detectCloudProvider(target.hostname);
1662
- if (cloudCat && !suggestions.find((s) => s.category === cloudCat)) {
1663
- const catTools = this.getByCategory(cloudCat);
1664
- suggestions.push({
1665
- category: cloudCat,
1666
- tools: catTools.map((t) => t.name)
1667
- });
1668
- }
1669
- }
1670
- return suggestions;
1671
- }
1672
- /**
1673
- * Detect category from port number and service name
1674
- */
1675
- detectCategoryFromPort(port, serviceName) {
1676
- if (PORT_CATEGORY_MAP[port]) {
1677
- return PORT_CATEGORY_MAP[port];
1678
- }
1679
- if (serviceName && SERVICE_CATEGORY_MAP[serviceName.toLowerCase()]) {
1680
- return SERVICE_CATEGORY_MAP[serviceName.toLowerCase()];
1681
- }
1682
- if (port >= 8e3 && port <= 9e3) return SERVICE_CATEGORIES.WEB;
1683
- if (port >= 3e3 && port <= 3500) return SERVICE_CATEGORIES.API;
1684
- return null;
1685
- }
1686
- /**
1687
- * Detect cloud provider from hostname
1688
- */
1689
- detectCloudProvider(hostname) {
1690
- const cloudProviders = [
1691
- "amazonaws.com",
1692
- "aws",
1693
- "s3.amazonaws.com",
1694
- "azure.com",
1695
- "azurewebsites.net",
1696
- "windows.net",
1697
- "gcp",
1698
- "googleusercontent.com",
1699
- "appspot.com",
1700
- "digitalocean.com",
1701
- "do.co",
1702
- "herokuapp.com",
1703
- "vercel.app",
1704
- "netlify.app"
1705
- ];
1706
- const lowerHostname = hostname.toLowerCase();
1707
- for (const provider of cloudProviders) {
1708
- if (lowerHostname.includes(provider)) {
1709
- return SERVICE_CATEGORIES.CLOUD;
1318
+ const results = [];
1319
+ const seen = /* @__PURE__ */ new Set();
1320
+ target.ports.forEach((p) => {
1321
+ const cat = ServiceParser.detectCategory(p.port, p.service);
1322
+ if (cat && !seen.has(cat)) {
1323
+ seen.add(cat);
1324
+ results.push({ category: cat, tools: this.getByCategory(cat).map((t) => t.name) });
1710
1325
  }
1326
+ });
1327
+ if (target.hostname && ServiceParser.isCloudHostname(target.hostname) && !seen.has(SERVICE_CATEGORIES.CLOUD)) {
1328
+ results.push({ category: SERVICE_CATEGORIES.CLOUD, tools: this.getByCategory(SERVICE_CATEGORIES.CLOUD).map((t) => t.name) });
1711
1329
  }
1712
- return null;
1330
+ return results;
1713
1331
  }
1714
- /**
1715
- * Suggest sub-agent type for target
1716
- */
1717
1332
  suggestSubAgent(target) {
1718
1333
  const suggestions = this.suggestTools(target);
1719
1334
  const priority = [
1720
1335
  SERVICE_CATEGORIES.ICS,
1721
- // Critical - block
1722
1336
  SERVICE_CATEGORIES.AD,
1723
- // High value - review
1724
1337
  SERVICE_CATEGORIES.DATABASE,
1725
- // High value - review
1726
1338
  SERVICE_CATEGORIES.CLOUD,
1727
- // High value - review
1728
1339
  SERVICE_CATEGORIES.CONTAINER,
1729
- // Escalation - review
1730
- SERVICE_CATEGORIES.REMOTE_ACCESS,
1731
- // Access - review
1732
1340
  SERVICE_CATEGORIES.WEB,
1733
- // Common - confirm
1734
- SERVICE_CATEGORIES.API,
1735
- // Common - confirm
1736
- SERVICE_CATEGORIES.EMAIL,
1737
- // Info - confirm
1738
- SERVICE_CATEGORIES.FILE_SHARING,
1739
- // Access - confirm
1740
- SERVICE_CATEGORIES.NETWORK,
1741
- // Basic - confirm
1742
- SERVICE_CATEGORIES.WIRELESS
1743
- // Special - review
1341
+ SERVICE_CATEGORIES.NETWORK
1744
1342
  ];
1745
1343
  for (const cat of priority) {
1746
- if (suggestions.find((s) => s.category === cat)) {
1747
- return cat;
1748
- }
1344
+ if (suggestions.some((s) => s.category === cat)) return cat;
1749
1345
  }
1750
- return PHASES.RECON;
1346
+ return AGENT_ROLES.RECON;
1751
1347
  }
1752
- /**
1753
- * Get approval level for category
1754
- */
1755
- getApprovalForCategory(category) {
1756
- return CATEGORY_APPROVAL2[category];
1757
- }
1758
- /**
1759
- * Check if category requires special handling
1760
- */
1761
- isHighRiskCategory(category) {
1762
- return [SERVICE_CATEGORIES.ICS, SERVICE_CATEGORIES.AD, SERVICE_CATEGORIES.DATABASE, SERVICE_CATEGORIES.CLOUD, SERVICE_CATEGORIES.CONTAINER].includes(category);
1763
- }
1764
- /**
1765
- * Get service fingerprint from port data
1766
- */
1767
1348
  fingerprintService(port) {
1768
- const category = this.detectCategoryFromPort(port.port, port.service);
1349
+ const category = ServiceParser.detectCategory(port.port, port.service);
1769
1350
  if (!category) return null;
1770
1351
  return {
1771
1352
  port: port.port,
1772
1353
  service: port.service,
1773
1354
  version: port.version,
1774
- banner: port.notes[0] || void 0,
1355
+ banner: port.notes[0],
1775
1356
  category,
1776
1357
  confidence: port.version ? 0.9 : 0.7
1777
1358
  };
1778
1359
  }
1779
- /**
1780
- * Get category description
1781
- */
1782
- getCategoryDescription(category) {
1783
- return CATEGORY_DESCRIPTIONS[category];
1360
+ getByCategory(cat) {
1361
+ return this.categories.get(cat)?.tools || [];
1362
+ }
1363
+ getAllCategories() {
1364
+ return Array.from(this.categories.values());
1365
+ }
1366
+ getApprovalForCategory(cat) {
1367
+ return CATEGORY_APPROVAL2[cat] || "confirm";
1784
1368
  }
1785
1369
  };
1786
1370
 
@@ -1946,6 +1530,21 @@ function getModel() {
1946
1530
 
1947
1531
  // src/engine/llm.ts
1948
1532
  import Anthropic from "@anthropic-ai/sdk";
1533
+
1534
+ // src/shared/constants/llm.ts
1535
+ var RETRY_CONFIG = {
1536
+ baseDelayMs: 2e3,
1537
+ maxDelayMs: 6e4,
1538
+ jitterMs: 1e3
1539
+ };
1540
+ var LLM_LIMITS = {
1541
+ nonStreamMaxTokens: 4096,
1542
+ streamMaxTokens: 8192,
1543
+ /** WHY: ~3.5 chars/token is a reasonable average for mixed English/CJK content */
1544
+ charsPerTokenEstimate: 3.5
1545
+ };
1546
+
1547
+ // src/engine/llm.ts
1949
1548
  var LLMClient = class {
1950
1549
  client;
1951
1550
  model;
@@ -1957,15 +1556,46 @@ var LLMClient = class {
1957
1556
  });
1958
1557
  this.model = getModel();
1959
1558
  }
1559
+ /** Non-streaming response with automatic retry */
1560
+ async generateResponse(messages, tools, systemPrompt, callbacks) {
1561
+ return this.withRetry(callbacks, () => this.executeNonStream(messages, tools, systemPrompt));
1562
+ }
1563
+ /** Streaming response with reasoning callbacks and automatic retry */
1564
+ async generateResponseStream(messages, tools, systemPrompt, callbacks) {
1565
+ return this.withRetry(callbacks, () => this.executeStream(messages, tools, systemPrompt, callbacks));
1566
+ }
1567
+ getModelName() {
1568
+ return this.model;
1569
+ }
1570
+ // ─── Retry Logic ─────────────────────────────────────────────
1960
1571
  /**
1961
- * Generate response from messages and tools (non-streaming)
1572
+ * Unified retry wrapper retries unconditionally on all errors,
1573
+ * respects AbortSignal for instant cancellation.
1962
1574
  */
1963
- async generateResponse(messages, tools, systemPrompt) {
1575
+ async withRetry(callbacks, operation) {
1576
+ let attempt = 0;
1577
+ const signal = callbacks?.abortSignal;
1578
+ while (true) {
1579
+ if (signal?.aborted) this.throwAbort();
1580
+ try {
1581
+ return await operation();
1582
+ } catch (error) {
1583
+ if (this.isAbortError(error, signal)) this.throwAbort();
1584
+ const delayMs = this.calculateRetryDelay(attempt);
1585
+ const errorMsg = this.extractErrorMessage(error);
1586
+ callbacks?.onRetry?.(attempt + 1, 0, delayMs, errorMsg);
1587
+ await this.sleep(delayMs, signal);
1588
+ attempt++;
1589
+ }
1590
+ }
1591
+ }
1592
+ // ─── Non-Streaming Execution ─────────────────────────────────
1593
+ async executeNonStream(messages, tools, systemPrompt) {
1964
1594
  const response = await this.client.messages.create({
1965
1595
  model: this.model,
1966
- max_tokens: 4096,
1596
+ max_tokens: LLM_LIMITS.nonStreamMaxTokens,
1967
1597
  system: systemPrompt,
1968
- messages: messages.filter((m) => m.role !== "system"),
1598
+ messages: this.filterSystemMessages(messages),
1969
1599
  tools
1970
1600
  });
1971
1601
  const textBlock = response.content.find((b) => b.type === "text");
@@ -1977,65 +1607,138 @@ var LLMClient = class {
1977
1607
  name: b.name,
1978
1608
  input: b.input
1979
1609
  })),
1980
- rawResponse: response
1610
+ rawResponse: response,
1611
+ usage: response.usage ? { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens } : void 0
1981
1612
  };
1982
1613
  }
1983
- /**
1984
- * Generate response with streaming support for real-time thinking display
1985
- * This is like opencode's reasoning stream handling
1986
- */
1987
- async generateResponseStream(messages, tools, systemPrompt, callbacks) {
1614
+ // ─── Streaming Execution ─────────────────────────────────────
1615
+ async executeStream(messages, tools, systemPrompt, callbacks) {
1988
1616
  const stream = await this.client.messages.create({
1989
1617
  model: this.model,
1990
- max_tokens: 4096,
1618
+ max_tokens: LLM_LIMITS.streamMaxTokens,
1991
1619
  system: systemPrompt,
1992
- messages: messages.filter((m) => m.role !== "system"),
1620
+ messages: this.filterSystemMessages(messages),
1993
1621
  tools,
1994
1622
  stream: true
1995
1623
  });
1996
1624
  let fullContent = "";
1625
+ let fullReasoning = "";
1997
1626
  const toolCallsMap = /* @__PURE__ */ new Map();
1998
- callbacks?.onThinkingStart?.(systemPrompt ? "processing" : "default", 0);
1627
+ let isReasoningBlock = false;
1628
+ let isTextBlock = false;
1629
+ let usage = { input_tokens: 0, output_tokens: 0 };
1630
+ let totalChars = 0;
1999
1631
  for await (const event of stream) {
2000
1632
  switch (event.type) {
2001
1633
  case "content_block_start":
2002
- if (event.content_block.type === "text") {
2003
- } else if (event.content_block.type === "tool_use") {
2004
- const toolId = event.content_block.id;
2005
- toolCallsMap.set(toolId, { id: toolId, name: "", input: {} });
2006
- }
1634
+ this.handleBlockStart(
1635
+ event,
1636
+ toolCallsMap,
1637
+ callbacks,
1638
+ () => {
1639
+ isTextBlock = true;
1640
+ },
1641
+ () => {
1642
+ isReasoningBlock = true;
1643
+ }
1644
+ );
2007
1645
  break;
2008
- case "content_block_delta":
2009
- if (event.delta.type === "text_delta") {
2010
- const text = event.delta.text;
2011
- fullContent += text;
2012
- callbacks?.onThinkingDelta?.(text);
2013
- } else if (event.delta.type === "input_json_delta") {
1646
+ case "content_block_delta": {
1647
+ const delta = event.delta;
1648
+ if (delta.type === "text_delta" && isTextBlock) {
1649
+ fullContent += delta.text;
1650
+ totalChars += delta.text.length;
1651
+ callbacks?.onOutputDelta?.(delta.text);
1652
+ } else if (delta.type === "thinking_delta" && isReasoningBlock) {
1653
+ const text = delta.thinking || "";
1654
+ fullReasoning += text;
1655
+ totalChars += text.length;
1656
+ callbacks?.onReasoningDelta?.(text);
1657
+ } else if (delta.type === "input_json_delta") {
1658
+ totalChars += (delta.partial_json || "").length;
2014
1659
  }
1660
+ const estimatedOutput = Math.ceil(totalChars / LLM_LIMITS.charsPerTokenEstimate);
1661
+ callbacks?.onUsageUpdate?.({ input_tokens: usage.input_tokens, output_tokens: estimatedOutput });
2015
1662
  break;
1663
+ }
2016
1664
  case "content_block_stop":
1665
+ if (isReasoningBlock) callbacks?.onReasoningEnd?.();
1666
+ if (isTextBlock) callbacks?.onOutputEnd?.();
1667
+ isReasoningBlock = false;
1668
+ isTextBlock = false;
2017
1669
  break;
2018
- case "message_start":
2019
- break;
2020
- case "message_delta":
1670
+ case "message_start": {
1671
+ const msg = event.message;
1672
+ if (msg?.usage) {
1673
+ usage = { ...msg.usage };
1674
+ callbacks?.onUsageUpdate?.({ ...usage });
1675
+ }
2021
1676
  break;
2022
- case "message_stop":
1677
+ }
1678
+ case "message_delta": {
1679
+ const deltaUsage = event.usage;
1680
+ if (deltaUsage) {
1681
+ usage.output_tokens = deltaUsage.output_tokens || 0;
1682
+ callbacks?.onUsageUpdate?.({ ...usage });
1683
+ }
2023
1684
  break;
1685
+ }
2024
1686
  }
2025
1687
  }
2026
- callbacks?.onThinkingEnd?.();
2027
1688
  const toolCalls = Array.from(toolCallsMap.values());
2028
1689
  return {
2029
1690
  content: fullContent,
2030
1691
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
2031
- rawResponse: null
1692
+ rawResponse: null,
1693
+ reasoning: fullReasoning || void 0,
1694
+ usage: usage.input_tokens > 0 || usage.output_tokens > 0 ? usage : void 0
2032
1695
  };
2033
1696
  }
2034
- /**
2035
- * Get current model name
2036
- */
2037
- getModelName() {
2038
- return this.model;
1697
+ // ─── Helpers ─────────────────────────────────────────────────
1698
+ handleBlockStart(event, toolCallsMap, callbacks, onText, onReasoning) {
1699
+ const blockType = event.content_block?.type;
1700
+ if (blockType === "text") {
1701
+ onText();
1702
+ callbacks?.onOutputStart?.();
1703
+ } else if (blockType === "thinking" || blockType === "reasoning") {
1704
+ onReasoning();
1705
+ callbacks?.onReasoningStart?.();
1706
+ } else if (blockType === "tool_use") {
1707
+ const block = event.content_block;
1708
+ toolCallsMap.set(block.id, { id: block.id, name: block.name || "", input: block.input || {} });
1709
+ }
1710
+ }
1711
+ filterSystemMessages(messages) {
1712
+ return messages.filter((m) => m.role !== "system");
1713
+ }
1714
+ calculateRetryDelay(attempt) {
1715
+ const exponentialDelay = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
1716
+ const jitter = Math.random() * RETRY_CONFIG.jitterMs;
1717
+ return Math.min(exponentialDelay + jitter, RETRY_CONFIG.maxDelayMs);
1718
+ }
1719
+ extractErrorMessage(error) {
1720
+ const e = error;
1721
+ return e?.error?.error?.message || e?.error?.message || e?.message || String(error);
1722
+ }
1723
+ /** Abortable sleep — rejects immediately if signal fires */
1724
+ sleep(ms, signal) {
1725
+ return new Promise((resolve, reject) => {
1726
+ if (signal?.aborted) {
1727
+ reject(new DOMException("Aborted", "AbortError"));
1728
+ return;
1729
+ }
1730
+ const timer = setTimeout(resolve, ms);
1731
+ signal?.addEventListener("abort", () => {
1732
+ clearTimeout(timer);
1733
+ reject(new DOMException("Aborted", "AbortError"));
1734
+ }, { once: true });
1735
+ });
1736
+ }
1737
+ isAbortError(error, signal) {
1738
+ return error?.name === "AbortError" || !!signal?.aborted;
1739
+ }
1740
+ throwAbort() {
1741
+ throw new DOMException("Aborted", "AbortError");
2039
1742
  }
2040
1743
  };
2041
1744
  var llmInstance = null;
@@ -2054,6 +1757,7 @@ var CoreAgent = class {
2054
1757
  toolRegistry;
2055
1758
  agentType;
2056
1759
  maxIterations;
1760
+ abortController = null;
2057
1761
  constructor(agentType, state, events, toolRegistry, maxIterations) {
2058
1762
  this.agentType = agentType;
2059
1763
  this.state = state;
@@ -2062,22 +1766,35 @@ var CoreAgent = class {
2062
1766
  this.llm = getLLMClient();
2063
1767
  this.maxIterations = maxIterations || AGENT_LIMITS.MAX_ITERATIONS;
2064
1768
  }
2065
- /**
2066
- * The core loop: think -> act -> observe
2067
- */
1769
+ /** Abort the current execution — immediately cancels LLM calls and retries */
1770
+ abort() {
1771
+ this.abortController?.abort();
1772
+ }
1773
+ /** The core loop: Think → Act → Observe */
2068
1774
  async run(task, systemPrompt) {
2069
- const messages = [{ role: "user", content: task }];
1775
+ this.abortController = new AbortController();
1776
+ const messages = [{ role: LLM_ROLES.USER, content: task }];
2070
1777
  let toolsExecutedCount = 0;
2071
1778
  for (let iteration = 0; iteration < this.maxIterations; iteration++) {
2072
- const result = await this.step(iteration, messages, systemPrompt);
2073
- toolsExecutedCount += result.toolsExecuted;
2074
- if (result.completed) {
2075
- return {
2076
- output: result.output,
2077
- iterations: iteration + 1,
2078
- toolsExecuted: toolsExecutedCount,
2079
- completed: true
2080
- };
1779
+ if (this.abortController.signal.aborted) {
1780
+ return this.buildCancelledResult(iteration, toolsExecutedCount);
1781
+ }
1782
+ try {
1783
+ const result = await this.step(iteration, messages, systemPrompt);
1784
+ toolsExecutedCount += result.toolsExecuted;
1785
+ if (result.completed) {
1786
+ return {
1787
+ output: result.output,
1788
+ iterations: iteration + 1,
1789
+ toolsExecuted: toolsExecutedCount,
1790
+ completed: true
1791
+ };
1792
+ }
1793
+ } catch (error) {
1794
+ if (this.isAbortError(error)) {
1795
+ return this.buildCancelledResult(iteration, toolsExecutedCount);
1796
+ }
1797
+ throw error;
2081
1798
  }
2082
1799
  }
2083
1800
  return {
@@ -2087,40 +1804,59 @@ var CoreAgent = class {
2087
1804
  completed: false
2088
1805
  };
2089
1806
  }
2090
- /**
2091
- * Execute a single iteration step with real-time thinking display
2092
- */
1807
+ // ─── Step Execution ──────────────────────────────────────────
1808
+ /** Execute a single Think Act Observe iteration */
2093
1809
  async step(iteration, messages, systemPrompt) {
2094
1810
  const phase = this.state.getPhase();
2095
- this.emitThinkingStart(phase, iteration);
1811
+ const stepStartTime = Date.now();
2096
1812
  this.emitThink(iteration);
1813
+ const callbacks = this.buildStreamCallbacks(phase);
2097
1814
  const response = await this.llm.generateResponseStream(
2098
1815
  messages,
2099
1816
  this.getToolSchemas(),
2100
1817
  systemPrompt,
2101
- {
2102
- onThinkingStart: (phase2, iter) => {
2103
- },
2104
- onThinkingDelta: (content) => {
2105
- this.emitThinkingDelta(content, phase);
2106
- },
2107
- onThinkingEnd: () => {
2108
- this.emitThinkingEnd(phase);
2109
- }
2110
- }
1818
+ callbacks
2111
1819
  );
2112
- messages.push({ role: "assistant", content: response.content });
2113
- if (!response.toolCalls || response.toolCalls.length === 0) {
2114
- this.emitComplete(response.content, iteration, 0);
1820
+ messages.push({ role: LLM_ROLES.ASSISTANT, content: response.content });
1821
+ const stepDuration = Date.now() - stepStartTime;
1822
+ const tokens = response.usage ? { input: response.usage.input_tokens, output: response.usage.output_tokens } : void 0;
1823
+ if (!response.toolCalls?.length) {
1824
+ this.emitComplete(response.content, iteration, 0, stepDuration, tokens);
2115
1825
  return { output: response.content, toolsExecuted: 0, completed: true };
2116
1826
  }
2117
1827
  const results = await this.processToolCalls(response.toolCalls);
2118
1828
  this.addToolResultsToMessages(messages, results);
2119
1829
  return { output: "", toolsExecuted: results.length, completed: false };
2120
1830
  }
1831
+ // ─── Callback Builder ────────────────────────────────────────
1832
+ buildStreamCallbacks(phase) {
1833
+ return {
1834
+ onReasoningStart: () => this.emitReasoningStart(phase),
1835
+ onReasoningDelta: (content) => this.emitReasoningDelta(content, phase),
1836
+ onReasoningEnd: () => this.emitReasoningEnd(phase),
1837
+ onOutputDelta: () => {
1838
+ },
1839
+ onRetry: (attempt, maxRetries, delayMs, error) => {
1840
+ this.events.emit({
1841
+ type: EVENT_TYPES.RETRY,
1842
+ timestamp: Date.now(),
1843
+ data: { attempt, maxRetries, delayMs, error, phase }
1844
+ });
1845
+ },
1846
+ onUsageUpdate: (usage) => {
1847
+ this.events.emit({
1848
+ type: EVENT_TYPES.USAGE_UPDATE,
1849
+ timestamp: Date.now(),
1850
+ data: { inputTokens: usage.input_tokens, outputTokens: usage.output_tokens }
1851
+ });
1852
+ },
1853
+ abortSignal: this.abortController?.signal
1854
+ };
1855
+ }
1856
+ // ─── Event Emitters ──────────────────────────────────────────
2121
1857
  emitThink(iteration) {
2122
1858
  this.events.emit({
2123
- type: "think",
1859
+ type: EVENT_TYPES.THINK,
2124
1860
  timestamp: Date.now(),
2125
1861
  data: {
2126
1862
  thought: `${this.agentType} agent iteration ${iteration + 1}: Decision making`,
@@ -2128,60 +1864,34 @@ var CoreAgent = class {
2128
1864
  }
2129
1865
  });
2130
1866
  }
2131
- /**
2132
- * Emit thinking start event - like opencode's reasoning-start
2133
- */
2134
- emitThinkingStart(phase, iteration) {
2135
- this.events.emit({
2136
- type: "thinking_start",
2137
- timestamp: Date.now(),
2138
- data: {
2139
- phase,
2140
- iteration
2141
- }
2142
- });
1867
+ /** Emit reasoning lifecycle events for extended thinking */
1868
+ emitReasoningStart(phase) {
1869
+ this.events.emit({ type: EVENT_TYPES.REASONING_START, timestamp: Date.now(), data: { phase } });
2143
1870
  }
2144
- /**
2145
- * Emit thinking delta event - like opencode's reasoning-delta
2146
- * This provides real-time transparency of the model's thinking process
2147
- */
2148
- emitThinkingDelta(content, phase) {
2149
- this.events.emit({
2150
- type: "thinking_delta",
2151
- timestamp: Date.now(),
2152
- data: {
2153
- content,
2154
- phase
2155
- }
2156
- });
1871
+ emitReasoningDelta(content, phase) {
1872
+ this.events.emit({ type: EVENT_TYPES.REASONING_DELTA, timestamp: Date.now(), data: { content, phase } });
2157
1873
  }
2158
- /**
2159
- * Emit thinking end event - like opencode's reasoning-end
2160
- */
2161
- emitThinkingEnd(phase) {
2162
- this.events.emit({
2163
- type: "thinking_end",
2164
- timestamp: Date.now(),
2165
- data: {
2166
- phase
2167
- }
2168
- });
1874
+ emitReasoningEnd(phase) {
1875
+ this.events.emit({ type: EVENT_TYPES.REASONING_END, timestamp: Date.now(), data: { phase } });
2169
1876
  }
2170
- emitComplete(output, iteration, toolsExecuted) {
1877
+ emitComplete(output, iteration, toolsExecuted, durationMs, tokens) {
2171
1878
  this.events.emit({
2172
- type: "complete",
1879
+ type: EVENT_TYPES.COMPLETE,
2173
1880
  timestamp: Date.now(),
2174
1881
  data: {
2175
1882
  finalOutput: output,
2176
1883
  iterations: iteration + 1,
2177
- toolsExecuted
1884
+ toolsExecuted,
1885
+ durationMs,
1886
+ tokens
2178
1887
  }
2179
1888
  });
2180
1889
  }
1890
+ // ─── Tool Processing ─────────────────────────────────────────
2181
1891
  addToolResultsToMessages(messages, results) {
2182
1892
  for (const res of results) {
2183
1893
  messages.push({
2184
- role: "user",
1894
+ role: LLM_ROLES.USER,
2185
1895
  content: [
2186
1896
  {
2187
1897
  type: "tool_result",
@@ -2196,10 +1906,11 @@ var CoreAgent = class {
2196
1906
  async processToolCalls(toolCalls) {
2197
1907
  const results = [];
2198
1908
  for (const call of toolCalls) {
2199
- const toolName = call.name;
2200
- const toolInput = call.input;
2201
1909
  try {
2202
- const result = await this.toolRegistry.execute({ name: toolName, input: toolInput });
1910
+ const result = await this.toolRegistry.execute({
1911
+ name: call.name,
1912
+ input: call.input
1913
+ });
2203
1914
  results.push({ toolCallId: call.id, output: result.output, error: result.error });
2204
1915
  } catch (error) {
2205
1916
  results.push({ toolCallId: call.id, output: String(error), error: String(error) });
@@ -2218,103 +1929,137 @@ var CoreAgent = class {
2218
1929
  }
2219
1930
  }));
2220
1931
  }
1932
+ // ─── Abort Helpers ───────────────────────────────────────────
1933
+ isAbortError(error) {
1934
+ return error?.name === "AbortError" || !!this.abortController?.signal.aborted;
1935
+ }
1936
+ buildCancelledResult(iteration, toolsExecuted) {
1937
+ return {
1938
+ output: "Operation cancelled by user",
1939
+ iterations: iteration,
1940
+ toolsExecuted,
1941
+ completed: false
1942
+ };
1943
+ }
2221
1944
  };
2222
1945
 
2223
1946
  // src/agents/prompt-builder.ts
2224
1947
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2225
1948
  import { join as join3, dirname as dirname2 } from "path";
2226
1949
  import { fileURLToPath as fileURLToPath3 } from "url";
1950
+
1951
+ // src/shared/constants/prompts.ts
1952
+ var PROMPT_XML = {
1953
+ PHASE: (phase, content) => `<phase-instructions phase="${phase}">
1954
+ ${content}
1955
+ </phase-instructions>`,
1956
+ SCOPE: (allowed, domains, exclude, flags) => `<scope type="ABSOLUTE_CONSTRAINT">
1957
+ Authorized CIDR: ${allowed}
1958
+ Authorized Domains: ${domains}
1959
+ Exclusions: ${exclude}
1960
+ Constraints: ${flags}
1961
+ </scope>`,
1962
+ STATE: (content) => `<current-state>
1963
+ ${content}
1964
+ </current-state>`,
1965
+ TODO: (content) => `<todo>
1966
+ ${content}
1967
+ </todo>`
1968
+ };
1969
+ var PROMPT_DEFAULTS = {
1970
+ NO_SCOPE: "<scope>NO SCOPE DEFINED. STOP.</scope>",
1971
+ EMPTY_TODO: "Create initial plan",
1972
+ USER_CONTEXT: (context) => `User Context: ${context}`
1973
+ };
1974
+ var PROMPT_PATHS = {
1975
+ ROOT: "../shared/prompts",
1976
+ PHASES: "phases",
1977
+ BASE: "base.md",
1978
+ FILE_EXT: ".md",
1979
+ AGENT_FILES: {
1980
+ ORCHESTRATOR: "orchestrator.md",
1981
+ RECON: "recon.md",
1982
+ VULN: "vuln.md",
1983
+ EXPLOIT: "exploit.md",
1984
+ POST: "post.md",
1985
+ REPORT: "report.md",
1986
+ INFRA: "infra.md"
1987
+ }
1988
+ };
1989
+ var PROMPT_CONFIG = {
1990
+ ENCODING: "utf-8"
1991
+ };
1992
+ var INITIAL_TASKS = {
1993
+ RECON: "Initial reconnaissance and target discovery"
1994
+ };
1995
+
1996
+ // src/agents/prompt-builder.ts
2227
1997
  var __dirname3 = dirname2(fileURLToPath3(import.meta.url));
2228
1998
  var PromptBuilder = class {
2229
1999
  state;
2230
2000
  promptDir;
2231
2001
  constructor(state) {
2232
2002
  this.state = state;
2233
- this.promptDir = join3(__dirname3, "../shared/prompts");
2003
+ this.promptDir = join3(__dirname3, PROMPT_PATHS.ROOT);
2234
2004
  }
2235
2005
  /**
2236
2006
  * Build complete prompt for LLM iteration
2007
+ * Implements §7-2 (Abstraction Level Consistency)
2237
2008
  */
2238
2009
  build(userInput, phase) {
2239
- return [
2240
- this.loadFragment("base.md"),
2010
+ const fragments = [
2011
+ this.loadFragment(PROMPT_PATHS.BASE),
2241
2012
  this.loadPhasePrompt(phase),
2242
2013
  this.getScopeFragment(),
2243
2014
  this.getStateFragment(),
2244
2015
  this.getTodoFragment(),
2245
- `User Context: ${userInput}`
2246
- ].join("\n\n");
2016
+ PROMPT_DEFAULTS.USER_CONTEXT(userInput)
2017
+ ];
2018
+ return fragments.filter((f) => !!f).join("\n\n");
2247
2019
  }
2248
2020
  loadFragment(filename) {
2249
2021
  const path2 = join3(this.promptDir, filename);
2250
- if (!existsSync2(path2)) return "";
2251
- return readFileSync2(path2, "utf-8");
2022
+ return existsSync2(path2) ? readFileSync2(path2, PROMPT_CONFIG.ENCODING) : "";
2252
2023
  }
2253
2024
  loadPhasePrompt(phase) {
2254
- const path2 = join3(this.promptDir, "phases", `${phase}.md`);
2255
- return existsSync2(path2) ? `<phase-instructions phase="${phase}">
2256
- ${readFileSync2(path2, "utf-8")}
2257
- </phase-instructions>` : "";
2025
+ const path2 = join3(this.promptDir, PROMPT_PATHS.PHASES, `${phase}${PROMPT_PATHS.FILE_EXT}`);
2026
+ return existsSync2(path2) ? PROMPT_XML.PHASE(phase, readFileSync2(path2, PROMPT_CONFIG.ENCODING)) : "";
2258
2027
  }
2259
2028
  getScopeFragment() {
2260
2029
  const scope = this.state.getScope();
2261
- if (!scope) return "<scope>NO SCOPE DEFINED. STOP.</scope>";
2262
- return `<scope type="ABSOLUTE_CONSTRAINT">
2263
- Authorized CIDR: ${scope.allowedCidrs.join(", ")}
2264
- Authorized Domains: ${scope.allowedDomains.join(", ")}
2265
- Exclusions: ${scope.exclusions.join(", ") || "none"}
2266
- NoDoS: ${scope.noDOS} | NoSocial: ${scope.noSocial}
2267
- </scope>`;
2030
+ if (!scope) return PROMPT_DEFAULTS.NO_SCOPE;
2031
+ const flags = `NoDoS: ${scope.noDOS} | NoSocial: ${scope.noSocial}`;
2032
+ return PROMPT_XML.SCOPE(
2033
+ scope.allowedCidrs.join(", ") || "none",
2034
+ scope.allowedDomains.join(", ") || "none",
2035
+ scope.exclusions.join(", ") || "none",
2036
+ flags
2037
+ );
2268
2038
  }
2269
2039
  getStateFragment() {
2270
- return `<current-state>
2271
- ${this.state.toPrompt()}
2272
- </current-state>`;
2040
+ return PROMPT_XML.STATE(this.state.toPrompt());
2273
2041
  }
2274
2042
  getTodoFragment() {
2275
2043
  const todo = this.state.getTodo();
2276
- const list = todo.map((t) => `[${t.status === "done" ? "x" : " "}] ${t.content} (${t.priority})`).join("\n");
2277
- return `<todo>
2278
- ${list || "Create initial plan"}
2279
- </todo>`;
2280
- }
2281
- };
2282
-
2283
- // src/agents/sub-agent-executor.ts
2284
- var SubAgentExecutor = class extends CoreAgent {
2285
- constructor(state, events, toolRegistry) {
2286
- super(AGENT_ROLES.RECON, state, events, toolRegistry);
2287
- }
2288
- /**
2289
- * Entry point for executing a sub-task
2290
- */
2291
- async executeSubTask(task, systemPrompt) {
2292
- console.log(`[SubAgent] Executing specialized task: ${task.slice(0, 50)}...`);
2293
- return await this.run(task, systemPrompt);
2044
+ const list = todo.map((t) => `[${t.status === TODO_STATUSES.DONE ? "x" : " "}] ${t.content} (${t.priority})`).join("\n");
2045
+ return PROMPT_XML.TODO(list || PROMPT_DEFAULTS.EMPTY_TODO);
2294
2046
  }
2295
2047
  };
2296
2048
 
2297
2049
  // src/agents/main-agent.ts
2298
2050
  var MainAgent = class extends CoreAgent {
2299
- promptBuilder;
2300
- approvalGate;
2301
- scopeGuard;
2302
- userInput = "";
2303
- constructor() {
2304
- const state = new SharedState();
2305
- const events = new AgentEventEmitter();
2306
- const approvalGate = new ApprovalGate(false);
2307
- const scopeGuard = new ScopeGuard(state);
2308
- const subAgentExecutor = new SubAgentExecutor(state, events, null);
2309
- const toolRegistry = new CategorizedToolRegistry(state, scopeGuard, approvalGate, events, subAgentExecutor);
2310
- subAgentExecutor.toolRegistry = toolRegistry;
2051
+ promptBuilder;
2052
+ approvalGate;
2053
+ scopeGuard;
2054
+ userInput = "";
2055
+ constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
2311
2056
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
2312
2057
  this.approvalGate = approvalGate;
2313
2058
  this.scopeGuard = scopeGuard;
2314
2059
  this.promptBuilder = new PromptBuilder(state);
2315
2060
  }
2316
2061
  /**
2317
- * Public entry point for running the agent with a simple string input
2062
+ * Public entry point for running the agent
2318
2063
  */
2319
2064
  async execute(userInput) {
2320
2065
  this.userInput = userInput;
@@ -2323,26 +2068,19 @@ var MainAgent = class extends CoreAgent {
2323
2068
  const result = await this.run(userInput, this.getCurrentPrompt());
2324
2069
  return result.output;
2325
2070
  }
2326
- /**
2327
- * Override CoreAgent.run to enforce dynamic prompt injection per iteration
2328
- * But keep the signature compatible with CoreAgent
2329
- */
2330
2071
  async run(task, _systemPrompt) {
2331
2072
  return super.run(task, _systemPrompt);
2332
2073
  }
2333
- /**
2334
- * Override step to ensure prompt is always fresh with latest state
2335
- */
2336
2074
  async step(iteration, messages, _unusedPrompt) {
2337
2075
  const dynamicPrompt = this.getCurrentPrompt();
2338
2076
  const result = await super.step(iteration, messages, dynamicPrompt);
2339
2077
  this.emitStateChange();
2340
2078
  return result;
2341
2079
  }
2342
- // --- Internal Helpers (Refactored for Readability) ---
2080
+ // --- Internal Helpers ---
2343
2081
  initializeTask() {
2344
2082
  if (this.state.getTodo().length === 0) {
2345
- this.state.addTodo("Initial reconnaissance and target discovery", PRIORITIES.HIGH);
2083
+ this.state.addTodo(INITIAL_TASKS.RECON, PRIORITIES.HIGH);
2346
2084
  }
2347
2085
  }
2348
2086
  getCurrentPrompt() {
@@ -2351,14 +2089,14 @@ var MainAgent = class extends CoreAgent {
2351
2089
  }
2352
2090
  emitStart(userInput) {
2353
2091
  this.events.emit({
2354
- type: "start",
2092
+ type: EVENT_TYPES.START,
2355
2093
  timestamp: Date.now(),
2356
2094
  data: { userInput, phase: this.state.getPhase() }
2357
2095
  });
2358
2096
  }
2359
2097
  emitStateChange() {
2360
2098
  this.events.emit({
2361
- type: "state_change",
2099
+ type: EVENT_TYPES.STATE_CHANGE,
2362
2100
  timestamp: Date.now(),
2363
2101
  data: {
2364
2102
  phase: this.state.getPhase(),
@@ -2403,6 +2141,43 @@ var MainAgent = class extends CoreAgent {
2403
2141
  }
2404
2142
  };
2405
2143
 
2144
+ // src/agents/sub-agent-executor.ts
2145
+ var SubAgentExecutor = class extends CoreAgent {
2146
+ constructor(state, events, toolRegistry) {
2147
+ super(AGENT_ROLES.RECON, state, events, toolRegistry);
2148
+ }
2149
+ /**
2150
+ * Entry point for executing a sub-task
2151
+ */
2152
+ async executeSubTask(task, systemPrompt) {
2153
+ console.log(`[SubAgent] Executing specialized task: ${task.slice(0, 50)}...`);
2154
+ return await this.run(task, systemPrompt);
2155
+ }
2156
+ };
2157
+
2158
+ // src/agents/factory.ts
2159
+ var AgentFactory = class {
2160
+ /**
2161
+ * Create a fully initialized MainAgent system
2162
+ */
2163
+ static createMainAgent(autoApprove = false) {
2164
+ const state = new SharedState();
2165
+ const events = new AgentEventEmitter();
2166
+ const approvalGate = new ApprovalGate(autoApprove);
2167
+ const scopeGuard = new ScopeGuard(state);
2168
+ const subAgentExecutor = new SubAgentExecutor(state, events, null);
2169
+ const toolRegistry = new CategorizedToolRegistry(
2170
+ state,
2171
+ scopeGuard,
2172
+ approvalGate,
2173
+ events,
2174
+ subAgentExecutor
2175
+ );
2176
+ subAgentExecutor.toolRegistry = toolRegistry;
2177
+ return new MainAgent(state, events, toolRegistry, approvalGate, scopeGuard);
2178
+ }
2179
+ };
2180
+
2406
2181
  // src/shared/constants/thought.ts
2407
2182
  var THOUGHT_TYPE = {
2408
2183
  THINKING: "thinking",
@@ -2555,378 +2330,586 @@ var THOUGHT_LABELS = {
2555
2330
  [THOUGHT_TYPE.BREAKTHROUGH]: "[!]"
2556
2331
  };
2557
2332
 
2558
- // src/platform/tui/components/footer.tsx
2559
- import { Box, Text } from "ink";
2560
- import { jsx, jsxs } from "react/jsx-runtime";
2561
- var Footer = ({ phase, targets, findings, todo, elapsedTime, isProcessing }) => {
2562
- return /* @__PURE__ */ jsxs(
2563
- Box,
2564
- {
2565
- paddingX: 1,
2566
- marginTop: 0,
2567
- justifyContent: "space-between",
2568
- children: [
2569
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
2570
- /* @__PURE__ */ jsxs(Text, { color: THEME.text.accent, children: [
2571
- "Phase: ",
2572
- /* @__PURE__ */ jsx(Text, { color: THEME.text.primary, children: phase })
2573
- ] }),
2574
- /* @__PURE__ */ jsxs(Text, { color: THEME.text.secondary, children: [
2575
- "Targets: ",
2576
- /* @__PURE__ */ jsx(Text, { color: THEME.text.primary, children: targets })
2577
- ] }),
2578
- /* @__PURE__ */ jsxs(Text, { color: THEME.status.warning, children: [
2579
- "Findings: ",
2580
- /* @__PURE__ */ jsx(Text, { color: THEME.text.primary, children: findings })
2581
- ] }),
2582
- /* @__PURE__ */ jsxs(Text, { color: THEME.text.muted, children: [
2583
- "Tasks: ",
2584
- /* @__PURE__ */ jsx(Text, { color: THEME.text.primary, children: todo })
2585
- ] })
2586
- ] }),
2587
- /* @__PURE__ */ jsxs(Box, { children: [
2588
- /* @__PURE__ */ jsx(Text, { color: isProcessing ? THEME.status.running : THEME.text.muted, children: isProcessing ? "\u25CF Running " : "\u25CB Idle " }),
2589
- /* @__PURE__ */ jsxs(Text, { color: THEME.text.secondary, children: [
2590
- Math.floor(elapsedTime / 60),
2591
- "m ",
2592
- Math.floor(elapsedTime % 60),
2593
- "s"
2594
- ] })
2595
- ] })
2596
- ]
2597
- }
2598
- );
2333
+ // src/platform/tui/constants/display.ts
2334
+ var DISPLAY_LIMITS2 = {
2335
+ reasoningBuffer: 1e3,
2336
+ reasoningPreview: 300,
2337
+ reasoningHistorySlice: 500,
2338
+ toolInputPreview: 50,
2339
+ toolInputTruncated: 47,
2340
+ toolOutputPreview: 300,
2341
+ statusThoughtPreview: 100,
2342
+ statusThoughtTruncated: 97,
2343
+ retryErrorPreview: 40,
2344
+ retryErrorTruncated: 37,
2345
+ timerInterval: 1e3,
2346
+ exitDelay: 100
2599
2347
  };
2600
- var footer_default = Footer;
2348
+ var MESSAGE_STYLES = {
2349
+ colors: {
2350
+ user: THEME.text.accent,
2351
+ assistant: THEME.text.primary,
2352
+ system: THEME.text.muted,
2353
+ error: THEME.status.error,
2354
+ tool: THEME.status.running,
2355
+ result: THEME.text.muted,
2356
+ reasoning: THEME.status.warning
2357
+ },
2358
+ prefixes: {
2359
+ user: "\u276F",
2360
+ assistant: ">",
2361
+ system: "\u2022",
2362
+ error: "\u2717",
2363
+ tool: " \u23BF",
2364
+ result: " \u2713",
2365
+ reasoning: "REASONING"
2366
+ }
2367
+ };
2368
+ var HELP_TEXT = `
2369
+ \u2500\u2500 Commands \u2500\u2500
2370
+ /target <ip> Set target
2371
+ /start [goal] Start autonomous pentest
2372
+ /stop Stop operation
2373
+ /findings Show findings
2374
+ /status Show status
2375
+ /clear Clear screen
2376
+ /exit Exit
2601
2377
 
2602
- // src/platform/tui/app.tsx
2603
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2604
- var App = ({ autoApprove = false, target }) => {
2605
- const { exit } = useApp();
2378
+ \u2500\u2500 Examples \u2500\u2500
2379
+ /target 192.168.1.1
2380
+ /start "Recon the target"
2381
+ /findings
2382
+ `;
2383
+
2384
+ // src/platform/tui/utils/format.ts
2385
+ var formatDuration = (ms) => {
2386
+ const totalSec = ms / 1e3;
2387
+ if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
2388
+ const minutes = Math.floor(totalSec / 60);
2389
+ const seconds = Math.floor(totalSec % 60);
2390
+ return `${minutes}m ${seconds}s`;
2391
+ };
2392
+ var formatTokens = (count) => {
2393
+ if (count >= 1e6) return (count / 1e6).toFixed(1) + "M";
2394
+ if (count >= 1e3) return (count / 1e3).toFixed(1) + "K";
2395
+ return String(count);
2396
+ };
2397
+ var formatMeta = (ms, tokens) => {
2398
+ const parts = [];
2399
+ if (ms > 0) parts.push(formatDuration(ms));
2400
+ if (tokens > 0) parts.push(`\u2191 ${formatTokens(tokens)} tokens`);
2401
+ return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
2402
+ };
2403
+
2404
+ // src/platform/tui/hooks/useAgent.ts
2405
+ var useAgent = (autoApprove, target) => {
2606
2406
  const [messages, setMessages] = useState([]);
2607
- const [input, setInput] = useState("");
2608
2407
  const [isProcessing, setIsProcessing] = useState(false);
2609
2408
  const [currentStatus, setCurrentStatus] = useState("");
2610
2409
  const [elapsedTime, setElapsedTime] = useState(0);
2611
- const [thinking, setThinking] = useState({ isActive: false, content: "", phase: "" });
2612
- const [stats, setStats] = useState({
2613
- phase: "init",
2614
- targets: 0,
2615
- findings: 0,
2616
- todo: 0
2410
+ const [reasoning, setReasoning] = useState({ isActive: false, content: "", phase: "" });
2411
+ const [retryState, setRetryState] = useState({
2412
+ isRetrying: false,
2413
+ attempt: 0,
2414
+ maxRetries: 0,
2415
+ delayMs: 0,
2416
+ error: "",
2417
+ countdown: 0
2617
2418
  });
2618
- const [agent] = useState(() => {
2619
- const ag = new MainAgent();
2620
- if (autoApprove) {
2621
- ag.setAutoApprove(true);
2622
- }
2623
- if (target) {
2624
- ag.addTarget(target);
2625
- ag.setScope([target]);
2626
- }
2627
- return ag;
2419
+ const [currentTokens, setCurrentTokens] = useState(0);
2420
+ const [inputRequest, setInputRequest] = useState({
2421
+ active: false,
2422
+ prompt: "",
2423
+ isPassword: false,
2424
+ resolve: null
2628
2425
  });
2426
+ const [stats, setStats] = useState({ phase: "init", targets: 0, findings: 0, todo: 0 });
2427
+ const lastResponseMetaRef = useRef(null);
2629
2428
  const startTimeRef = useRef(0);
2630
2429
  const timerRef = useRef(null);
2631
- const startTimer = useCallback(() => {
2632
- startTimeRef.current = Date.now();
2633
- timerRef.current = setInterval(() => {
2634
- setElapsedTime(Math.floor((Date.now() - startTimeRef.current) / 1e3));
2635
- }, 1e3);
2636
- }, []);
2637
- const stopTimer = useCallback(() => {
2638
- if (timerRef.current) {
2639
- clearInterval(timerRef.current);
2640
- timerRef.current = null;
2430
+ const retryCountdownRef = useRef(null);
2431
+ const retryCountRef = useRef(0);
2432
+ const tokenAccumRef = useRef(0);
2433
+ const lastStepTokensRef = useRef(0);
2434
+ const [agent] = useState(() => AgentFactory.createMainAgent(autoApprove));
2435
+ useEffect(() => {
2436
+ if (target) {
2437
+ agent.addTarget(target);
2438
+ agent.setScope([target]);
2641
2439
  }
2642
- const duration = Math.floor((Date.now() - startTimeRef.current) / 100) / 10;
2643
- setElapsedTime(0);
2644
- return duration;
2645
- }, []);
2440
+ }, [agent, target]);
2441
+ const eventsRef = useRef(agent.getEventEmitter());
2646
2442
  const addMessage = useCallback((type, content) => {
2647
2443
  const id = Math.random().toString(36).substring(7);
2648
2444
  setMessages((prev) => [...prev, { id, type, content, timestamp: /* @__PURE__ */ new Date() }]);
2649
2445
  }, []);
2650
- useEffect(() => {
2651
- if (target) {
2652
- addMessage("system", `Target: ${target}`);
2446
+ const resetCumulativeCounters = useCallback(() => {
2447
+ setCurrentTokens(0);
2448
+ retryCountRef.current = 0;
2449
+ tokenAccumRef.current = 0;
2450
+ lastStepTokensRef.current = 0;
2451
+ lastResponseMetaRef.current = null;
2452
+ }, []);
2453
+ const manageTimer = useCallback((action) => {
2454
+ if (action === "start") {
2455
+ startTimeRef.current = Date.now();
2456
+ timerRef.current = setInterval(() => {
2457
+ setElapsedTime(Math.floor((Date.now() - startTimeRef.current) / 1e3));
2458
+ }, DISPLAY_LIMITS2.timerInterval);
2459
+ } else {
2460
+ if (timerRef.current) clearInterval(timerRef.current);
2461
+ timerRef.current = null;
2462
+ setElapsedTime(0);
2653
2463
  }
2654
- if (autoApprove) {
2655
- addMessage("system", "YOLO Mode: Auto-approving all tool executions");
2464
+ }, []);
2465
+ const executeTask = useCallback(async (task) => {
2466
+ setIsProcessing(true);
2467
+ manageTimer("start");
2468
+ setCurrentStatus("Thinking");
2469
+ resetCumulativeCounters();
2470
+ try {
2471
+ const response = await agent.execute(task);
2472
+ const meta = lastResponseMetaRef.current;
2473
+ const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
2474
+ addMessage(UI_ROLES.AI, response + suffix);
2475
+ } catch (e) {
2476
+ addMessage(UI_ROLES.ERROR, e instanceof Error ? e.message : String(e));
2477
+ } finally {
2478
+ manageTimer("stop");
2479
+ setIsProcessing(false);
2480
+ setCurrentStatus("");
2656
2481
  }
2657
- const events = agent.getEventEmitter();
2658
- events.on("thinking_start", (event) => {
2659
- setThinking({ isActive: true, content: "", phase: event.data.phase });
2660
- });
2661
- events.on("thinking_delta", (event) => {
2662
- setThinking((prev) => {
2663
- const newContent = prev.content + event.data.content;
2664
- return {
2665
- ...prev,
2666
- content: newContent.slice(-500)
2667
- // Keep only last 500 chars for display
2668
- };
2669
- });
2670
- });
2671
- events.on("thinking_end", (event) => {
2672
- setThinking((prev) => ({ ...prev, isActive: false }));
2673
- });
2674
- events.on("tool_call", (event) => {
2675
- const data = event.data;
2676
- setCurrentStatus(`Executing: ${data.toolName}`);
2482
+ }, [agent, addMessage, manageTimer, resetCumulativeCounters]);
2483
+ const abort = useCallback(() => {
2484
+ agent.abort();
2485
+ setIsProcessing(false);
2486
+ manageTimer("stop");
2487
+ setCurrentStatus("");
2488
+ addMessage(UI_ROLES.SYSTEM, "Interrupted");
2489
+ }, [agent, addMessage, manageTimer]);
2490
+ const cancelInputRequest = useCallback(() => {
2491
+ if (inputRequest.active && inputRequest.resolve) {
2492
+ inputRequest.resolve(null);
2493
+ setInputRequest({ active: false, prompt: "", isPassword: false, resolve: null });
2494
+ addMessage(UI_ROLES.SYSTEM, "Input cancelled");
2495
+ }
2496
+ }, [inputRequest, addMessage]);
2497
+ useEffect(() => {
2498
+ const events = eventsRef.current;
2499
+ const onToolCall = (e) => {
2500
+ setCurrentStatus(`Executing: ${e.data.toolName}`);
2677
2501
  let inputStr = "";
2678
2502
  try {
2679
- if (data.input) {
2680
- const str = JSON.stringify(data.input);
2681
- inputStr = str.length > 50 ? str.substring(0, 47) + "..." : str;
2503
+ if (e.data.input) {
2504
+ const str = JSON.stringify(e.data.input);
2505
+ inputStr = str.length > DISPLAY_LIMITS2.toolInputPreview ? str.substring(0, DISPLAY_LIMITS2.toolInputTruncated) + "..." : str;
2682
2506
  }
2683
- } catch (e) {
2507
+ } catch {
2684
2508
  }
2685
- addMessage("tool", ` \u23BF ${data.toolName} ${inputStr}`);
2686
- });
2687
- events.on("tool_result", (event) => {
2688
- const data = event.data;
2689
- const icon = data.success ? "\u2713" : "\u2717";
2690
- const output = data.output || "";
2691
- const preview = output.slice(0, 300).replace(/\n/g, " ");
2692
- const more = output.length > 300 ? "..." : "";
2509
+ addMessage("tool", ` \u23BF ${e.data.toolName} ${inputStr}`);
2510
+ };
2511
+ const onToolResult = (e) => {
2512
+ const icon = e.data.success ? "\u2713" : "\u2717";
2513
+ const preview = (e.data.output || "").slice(0, DISPLAY_LIMITS2.toolOutputPreview).replace(/\n/g, " ");
2514
+ const more = (e.data.output || "").length > DISPLAY_LIMITS2.toolOutputPreview ? "..." : "";
2693
2515
  addMessage("result", ` ${icon} ${preview}${more}`);
2516
+ };
2517
+ const onReasoningStart = (e) => setReasoning({ isActive: true, content: "", phase: e.data.phase });
2518
+ const onReasoningDelta = (e) => setReasoning((prev) => ({
2519
+ ...prev,
2520
+ content: (prev.content + e.data.content).slice(-DISPLAY_LIMITS2.reasoningBuffer)
2521
+ }));
2522
+ const onReasoningEnd = () => setReasoning((prev) => {
2523
+ if (prev.content) addMessage("reasoning", `REASONING ${prev.content.slice(-DISPLAY_LIMITS2.reasoningHistorySlice)}`);
2524
+ return { ...prev, isActive: false };
2694
2525
  });
2695
- events.on("think", (event) => {
2696
- const data = event.data;
2697
- const thought = data.thought.length > 100 ? data.thought.substring(0, 97) + "..." : data.thought;
2698
- setCurrentStatus(thought);
2699
- });
2700
- events.on("complete", (event) => {
2701
- const data = event.data;
2702
- addMessage("system", `\u2713 Complete (${data.toolsExecuted} tools)`);
2703
- });
2704
- events.on("error", (event) => {
2705
- const data = event.data;
2706
- addMessage("error", data.message || "An error occurred");
2526
+ const onComplete = (e) => {
2527
+ addMessage("system", `\u2713 Complete ${formatMeta(e.data.durationMs || 0, (e.data.tokens?.input || 0) + (e.data.tokens?.output || 0))}`);
2528
+ lastResponseMetaRef.current = { durationMs: e.data.durationMs, tokens: e.data.tokens };
2529
+ };
2530
+ const onRetry = (e) => {
2531
+ const delaySec = Math.ceil(e.data.delayMs / 1e3);
2532
+ retryCountRef.current += 1;
2533
+ const retryNum = retryCountRef.current;
2534
+ addMessage("system", `\u27F3 Retry #${retryNum} \xB7 ${e.data.error} \xB7 waiting ${delaySec}s...`);
2535
+ setRetryState({ isRetrying: true, attempt: retryNum, maxRetries: e.data.maxRetries, delayMs: e.data.delayMs, error: e.data.error, countdown: delaySec });
2536
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
2537
+ let remaining = delaySec;
2538
+ retryCountdownRef.current = setInterval(() => {
2539
+ remaining -= 1;
2540
+ if (remaining <= 0) {
2541
+ setRetryState((prev) => ({ ...prev, isRetrying: false, countdown: 0 }));
2542
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
2543
+ } else {
2544
+ setRetryState((prev) => ({ ...prev, countdown: remaining }));
2545
+ }
2546
+ }, 1e3);
2547
+ };
2548
+ const onUsageUpdate = (e) => {
2549
+ const stepTokens = e.data.inputTokens + e.data.outputTokens;
2550
+ if (stepTokens < lastStepTokensRef.current) tokenAccumRef.current += lastStepTokensRef.current;
2551
+ lastStepTokensRef.current = stepTokens;
2552
+ setCurrentTokens(tokenAccumRef.current + stepTokens);
2553
+ };
2554
+ setInputHandler((p) => {
2555
+ return new Promise((resolve) => setInputRequest({ active: true, prompt: p.trim(), isPassword: /password|passphrase/i.test(p), resolve }));
2707
2556
  });
2708
2557
  const updateStats = () => {
2709
- const state = agent.getState();
2710
- setStats({
2711
- phase: agent.getPhase(),
2712
- targets: state.getTargets().size,
2713
- findings: state.getFindings().length,
2714
- todo: state.getTodo().length
2715
- });
2558
+ const s = agent.getState();
2559
+ setStats({ phase: agent.getPhase(), targets: s.getTargets().size, findings: s.getFindings().length, todo: s.getTodo().length });
2716
2560
  };
2561
+ events.on("tool_call", onToolCall);
2562
+ events.on("tool_result", onToolResult);
2563
+ events.on("reasoning_start", onReasoningStart);
2564
+ events.on("reasoning_delta", onReasoningDelta);
2565
+ events.on("reasoning_end", onReasoningEnd);
2566
+ events.on("think", (e) => {
2567
+ const t = e.data.thought;
2568
+ setCurrentStatus(t.length > DISPLAY_LIMITS2.statusThoughtPreview ? t.substring(0, DISPLAY_LIMITS2.statusThoughtTruncated) + "..." : t);
2569
+ });
2570
+ events.on("complete", onComplete);
2571
+ events.on("error", (e) => addMessage("error", e.data.message || "An error occurred"));
2572
+ events.on("retry", onRetry);
2573
+ events.on("usage_update", onUsageUpdate);
2717
2574
  events.on("state_change", updateStats);
2718
2575
  events.on("start", updateStats);
2719
- events.on("complete", updateStats);
2720
2576
  updateStats();
2721
2577
  return () => {
2578
+ events.removeAllListeners();
2722
2579
  if (timerRef.current) clearInterval(timerRef.current);
2723
- events.off("state_change", updateStats);
2724
- events.off("start", updateStats);
2725
- events.off("complete", updateStats);
2580
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
2726
2581
  };
2727
- }, [agent, target, autoApprove, addMessage, startTimer]);
2728
- const handleExit = useCallback(async () => {
2729
- setCurrentStatus("Exiting");
2730
- if (timerRef.current) {
2731
- clearInterval(timerRef.current);
2582
+ }, [agent, addMessage]);
2583
+ return {
2584
+ agent,
2585
+ messages,
2586
+ setMessages,
2587
+ isProcessing,
2588
+ currentStatus,
2589
+ elapsedTime,
2590
+ reasoning,
2591
+ retryState,
2592
+ currentTokens,
2593
+ inputRequest,
2594
+ setInputRequest,
2595
+ stats,
2596
+ executeTask,
2597
+ abort,
2598
+ cancelInputRequest,
2599
+ addMessage
2600
+ };
2601
+ };
2602
+
2603
+ // src/platform/tui/components/MessageList.tsx
2604
+ import { Box, Text, Static } from "ink";
2605
+ import { jsx, jsxs } from "react/jsx-runtime";
2606
+ var MessageList = ({ messages }) => {
2607
+ return /* @__PURE__ */ jsx(Static, { items: messages, children: (msg) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: MESSAGE_STYLES.colors[msg.type], dimColor: msg.type === "result", children: [
2608
+ MESSAGE_STYLES.prefixes[msg.type],
2609
+ " ",
2610
+ msg.content
2611
+ ] }) }, msg.id) });
2612
+ };
2613
+
2614
+ // src/platform/tui/components/StatusDisplay.tsx
2615
+ import { Box as Box2, Text as Text2 } from "ink";
2616
+ import Spinner from "ink-spinner";
2617
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2618
+ var StatusDisplay = ({
2619
+ reasoning,
2620
+ retryState,
2621
+ isProcessing,
2622
+ currentStatus,
2623
+ elapsedTime,
2624
+ currentTokens
2625
+ }) => {
2626
+ const truncateError = (err) => {
2627
+ return err.length > DISPLAY_LIMITS2.retryErrorPreview ? err.substring(0, DISPLAY_LIMITS2.retryErrorTruncated) + "..." : err;
2628
+ };
2629
+ const meta = formatMeta(elapsedTime * 1e3, currentTokens);
2630
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 0, children: [
2631
+ reasoning.isActive && reasoning.content && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, flexDirection: "column", children: [
2632
+ /* @__PURE__ */ jsxs2(Box2, { children: [
2633
+ /* @__PURE__ */ jsx2(Text2, { color: THEME.status.warning, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
2634
+ /* @__PURE__ */ jsx2(Text2, { color: THEME.status.warning, bold: true, children: " REASONING" }),
2635
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2636
+ " ",
2637
+ meta
2638
+ ] })
2639
+ ] }),
2640
+ /* @__PURE__ */ jsx2(Text2, { color: THEME.text.secondary, dimColor: true, children: reasoning.content.slice(-DISPLAY_LIMITS2.reasoningPreview) })
2641
+ ] }),
2642
+ retryState.isRetrying && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
2643
+ /* @__PURE__ */ jsx2(Text2, { color: THEME.status.warning, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
2644
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.status.warning, children: [
2645
+ " \u27F3 Retry #",
2646
+ retryState.attempt
2647
+ ] }),
2648
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2649
+ " \xB7 ",
2650
+ truncateError(retryState.error)
2651
+ ] }),
2652
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.accent.cyan, bold: true, children: [
2653
+ " \xB7 ",
2654
+ retryState.countdown,
2655
+ "s"
2656
+ ] })
2657
+ ] }),
2658
+ isProcessing && !reasoning.isActive && !retryState.isRetrying && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
2659
+ /* @__PURE__ */ jsx2(Text2, { color: THEME.spinner, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
2660
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2661
+ " ",
2662
+ currentStatus || "Processing"
2663
+ ] }),
2664
+ /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2665
+ " ",
2666
+ meta
2667
+ ] })
2668
+ ] })
2669
+ ] });
2670
+ };
2671
+
2672
+ // src/platform/tui/components/ChatInput.tsx
2673
+ import { Box as Box3, Text as Text3 } from "ink";
2674
+ import TextInput from "ink-text-input";
2675
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2676
+ var ChatInput = ({
2677
+ value,
2678
+ onChange,
2679
+ onSubmit,
2680
+ placeholder,
2681
+ inputRequest,
2682
+ secretInput,
2683
+ setSecretInput,
2684
+ onSecretSubmit
2685
+ }) => {
2686
+ return /* @__PURE__ */ jsx3(
2687
+ Box3,
2688
+ {
2689
+ borderStyle: "single",
2690
+ borderColor: inputRequest.active ? THEME.status.warning : THEME.border.default,
2691
+ paddingX: 1,
2692
+ children: inputRequest.active ? /* @__PURE__ */ jsxs3(Box3, { children: [
2693
+ /* @__PURE__ */ jsx3(Text3, { color: THEME.status.warning, children: "\u{1F512}" }),
2694
+ /* @__PURE__ */ jsxs3(Text3, { color: THEME.text.muted, children: [
2695
+ " ",
2696
+ inputRequest.prompt,
2697
+ " "
2698
+ ] }),
2699
+ /* @__PURE__ */ jsx3(
2700
+ TextInput,
2701
+ {
2702
+ value: secretInput,
2703
+ onChange: setSecretInput,
2704
+ onSubmit: onSecretSubmit,
2705
+ placeholder: "Enter input...",
2706
+ mask: inputRequest.isPassword ? "\u2022" : void 0
2707
+ }
2708
+ )
2709
+ ] }) : /* @__PURE__ */ jsxs3(Box3, { children: [
2710
+ /* @__PURE__ */ jsx3(Text3, { color: THEME.text.secondary, children: "\u25B8" }),
2711
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
2712
+ /* @__PURE__ */ jsx3(
2713
+ TextInput,
2714
+ {
2715
+ value,
2716
+ onChange,
2717
+ onSubmit,
2718
+ placeholder
2719
+ }
2720
+ )
2721
+ ] })
2722
+ }
2723
+ );
2724
+ };
2725
+
2726
+ // src/platform/tui/components/footer.tsx
2727
+ import { Box as Box4, Text as Text4 } from "ink";
2728
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2729
+ var formatElapsed = (totalSeconds) => {
2730
+ const hours = Math.floor(totalSeconds / 3600);
2731
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
2732
+ const seconds = Math.floor(totalSeconds % 60);
2733
+ const pad = (n) => String(n).padStart(2, "0");
2734
+ if (hours > 0) {
2735
+ return `${hours}:${pad(minutes)}:${pad(seconds)}`;
2736
+ }
2737
+ return `${minutes}:${pad(seconds)}`;
2738
+ };
2739
+ var Footer = ({ phase, targets, findings, todo, elapsedTime, isProcessing }) => {
2740
+ return /* @__PURE__ */ jsxs4(
2741
+ Box4,
2742
+ {
2743
+ paddingX: 1,
2744
+ marginTop: 0,
2745
+ justifyContent: "space-between",
2746
+ children: [
2747
+ /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
2748
+ /* @__PURE__ */ jsxs4(Text4, { color: THEME.text.secondary, children: [
2749
+ "Phase: ",
2750
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.primary, children: phase })
2751
+ ] }),
2752
+ /* @__PURE__ */ jsxs4(Text4, { color: THEME.text.secondary, children: [
2753
+ "Targets: ",
2754
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.primary, children: targets })
2755
+ ] }),
2756
+ /* @__PURE__ */ jsxs4(Text4, { color: THEME.text.secondary, children: [
2757
+ "Findings: ",
2758
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.primary, children: findings })
2759
+ ] }),
2760
+ /* @__PURE__ */ jsxs4(Text4, { color: THEME.text.secondary, children: [
2761
+ "Tasks: ",
2762
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.primary, children: todo })
2763
+ ] })
2764
+ ] }),
2765
+ /* @__PURE__ */ jsxs4(Box4, { children: [
2766
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.muted, children: isProcessing ? "Running " : "Idle " }),
2767
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.text.secondary, children: formatElapsed(elapsedTime) })
2768
+ ] })
2769
+ ]
2732
2770
  }
2771
+ );
2772
+ };
2773
+ var footer_default = Footer;
2774
+
2775
+ // src/platform/tui/app.tsx
2776
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2777
+ var App = ({ autoApprove = false, target }) => {
2778
+ const { exit } = useApp();
2779
+ const [input, setInput] = useState2("");
2780
+ const [secretInput, setSecretInput] = useState2("");
2781
+ const {
2782
+ agent,
2783
+ messages,
2784
+ setMessages,
2785
+ isProcessing,
2786
+ currentStatus,
2787
+ elapsedTime,
2788
+ reasoning,
2789
+ retryState,
2790
+ currentTokens,
2791
+ inputRequest,
2792
+ stats,
2793
+ executeTask,
2794
+ abort,
2795
+ cancelInputRequest,
2796
+ addMessage
2797
+ } = useAgent(autoApprove, target);
2798
+ const handleExit = useCallback2(() => {
2799
+ if (inputRequest.active && inputRequest.resolve) inputRequest.resolve(null);
2733
2800
  exit();
2734
- }, [exit]);
2735
- useEffect(() => {
2736
- const onExit = () => {
2737
- handleExit();
2738
- };
2739
- process.on("SIGINT", onExit);
2740
- process.on("SIGTERM", onExit);
2741
- return () => {
2742
- process.off("SIGINT", onExit);
2743
- process.off("SIGTERM", onExit);
2744
- };
2745
- }, [handleExit, exit]);
2746
- const handleSubmit = useCallback(async (value) => {
2801
+ setTimeout(() => process.exit(0), DISPLAY_LIMITS2.exitDelay);
2802
+ }, [exit, inputRequest, cancelInputRequest]);
2803
+ const handleCommand = useCallback2(async (cmd, args) => {
2804
+ switch (cmd) {
2805
+ case UI_COMMANDS.HELP:
2806
+ case UI_COMMANDS.HELP_SHORT:
2807
+ addMessage(UI_ROLES.SYSTEM, HELP_TEXT);
2808
+ break;
2809
+ case UI_COMMANDS.CLEAR:
2810
+ case UI_COMMANDS.CLEAR_SHORT:
2811
+ setMessages([]);
2812
+ break;
2813
+ case UI_COMMANDS.TARGET:
2814
+ case UI_COMMANDS.TARGET_SHORT:
2815
+ if (!args[0]) {
2816
+ addMessage(UI_ROLES.ERROR, "Usage: /target <ip>");
2817
+ break;
2818
+ }
2819
+ agent.addTarget(args[0]);
2820
+ agent.setScope([args[0]]);
2821
+ addMessage(UI_ROLES.SYSTEM, `Target \u2192 ${args[0]}`);
2822
+ break;
2823
+ case UI_COMMANDS.START:
2824
+ case UI_COMMANDS.START_SHORT:
2825
+ if (!agent.getState().getTargets().size) {
2826
+ addMessage(UI_ROLES.ERROR, "Set target first: /target <ip>");
2827
+ break;
2828
+ }
2829
+ executeTask(args.join(" ") || "Perform comprehensive penetration testing");
2830
+ break;
2831
+ case UI_COMMANDS.FINDINGS:
2832
+ case UI_COMMANDS.FINDINGS_SHORT:
2833
+ const findings = agent.getState().getFindings();
2834
+ if (!findings.length) {
2835
+ addMessage(UI_ROLES.SYSTEM, "No findings.");
2836
+ break;
2837
+ }
2838
+ addMessage(UI_ROLES.SYSTEM, `--- ${findings.length} Findings ---`);
2839
+ findings.forEach((f) => addMessage(UI_ROLES.SYSTEM, `[${f.severity}] ${f.title}`));
2840
+ break;
2841
+ case UI_COMMANDS.EXIT:
2842
+ case UI_COMMANDS.QUIT:
2843
+ case UI_COMMANDS.EXIT_SHORT:
2844
+ handleExit();
2845
+ break;
2846
+ default:
2847
+ addMessage(UI_ROLES.ERROR, `Unknown command: /${cmd}`);
2848
+ }
2849
+ }, [agent, addMessage, executeTask, setMessages, handleExit]);
2850
+ const handleSubmit = useCallback2(async (value) => {
2747
2851
  const trimmed = value.trim();
2748
2852
  if (!trimmed) return;
2749
2853
  setInput("");
2750
- addMessage("user", trimmed);
2854
+ addMessage(UI_ROLES.USER, trimmed);
2751
2855
  if (trimmed.startsWith("/")) {
2752
2856
  const [cmd, ...args] = trimmed.slice(1).split(" ");
2753
- switch (cmd) {
2754
- case "help":
2755
- case "h":
2756
- addMessage("system", `
2757
- \u2500\u2500 Commands \u2500\u2500
2758
- /target <ip> Set target
2759
- /start [goal] Start autonomous pentest
2760
- /stop Stop operation
2761
- /findings Show findings
2762
- /status Show status
2763
- /clear Clear screen
2764
- /exit Exit
2765
-
2766
- \u2500\u2500 Examples \u2500\u2500
2767
- /target 192.168.1.1
2768
- /start "Recon the target"
2769
- /findings
2770
- `);
2771
- return;
2772
- case "target":
2773
- case "t":
2774
- if (args[0]) {
2775
- agent.addTarget(args[0]);
2776
- agent.setScope([args[0]]);
2777
- addMessage("system", `Target \u2192 ${args[0]}`);
2778
- } else {
2779
- addMessage("system", "Usage: /target <ip>");
2780
- }
2781
- return;
2782
- case "start":
2783
- case "s":
2784
- if (!agent.getState().getTargets().size) {
2785
- addMessage("error", "Set target first: /target <ip>");
2786
- return;
2787
- }
2788
- setIsProcessing(true);
2789
- startTimer();
2790
- setCurrentStatus("Initializing");
2791
- addMessage("system", `Starting: ${args.join(" ") || "Perform comprehensive penetration testing"}`);
2792
- try {
2793
- await agent.execute(args.join(" ") || "Perform comprehensive penetration testing");
2794
- } catch (e) {
2795
- addMessage("error", e instanceof Error ? e.message : String(e));
2796
- }
2797
- stopTimer();
2798
- setIsProcessing(false);
2799
- setCurrentStatus("");
2800
- return;
2801
- case "stop":
2802
- addMessage("system", "Stopping...");
2803
- setIsProcessing(false);
2804
- setCurrentStatus("");
2805
- return;
2806
- case "findings":
2807
- case "f":
2808
- const findings = agent.getState().getFindings();
2809
- if (findings.length === 0) {
2810
- addMessage("system", "No findings.");
2811
- } else {
2812
- addMessage("system", `--- ${findings.length} Findings ---`);
2813
- findings.forEach((f) => addMessage("system", `[${f.severity}] ${f.title}`));
2814
- }
2815
- return;
2816
- case "status":
2817
- const state = agent.getState();
2818
- addMessage("system", `Status:
2819
- Phase: ${agent.getPhase()}
2820
- Targets: ${state.getTargets().size}
2821
- Findings: ${state.getFindings().length}
2822
- TODO: ${state.getTodo().length}
2823
- `);
2824
- return;
2825
- case "clear":
2826
- case "c":
2827
- setMessages([]);
2828
- return;
2829
- case "exit":
2830
- case "quit":
2831
- case "q":
2832
- await handleExit();
2833
- return;
2834
- default:
2835
- addMessage("error", `Unknown command: /${cmd}`);
2836
- return;
2837
- }
2838
- }
2839
- setIsProcessing(true);
2840
- startTimer();
2841
- setCurrentStatus("Thinking");
2842
- try {
2843
- const response = await agent.execute(trimmed);
2844
- addMessage("assistant", response);
2845
- } catch (e) {
2846
- addMessage("error", e instanceof Error ? e.message : String(e));
2847
- } finally {
2848
- stopTimer();
2849
- setIsProcessing(false);
2850
- setCurrentStatus("");
2851
- }
2852
- }, [agent, addMessage, startTimer, stopTimer, handleExit]);
2853
- useInput((input2, key) => {
2854
- if (key.ctrl && input2 === "c") {
2855
- if (isProcessing) {
2856
- setIsProcessing(false);
2857
- stopTimer();
2858
- setCurrentStatus("");
2859
- addMessage("system", "Interrupted");
2860
- } else {
2861
- exit();
2862
- }
2863
- return;
2864
- }
2857
+ await handleCommand(cmd, args);
2858
+ } else {
2859
+ await executeTask(trimmed);
2860
+ }
2861
+ }, [addMessage, executeTask, handleCommand]);
2862
+ const handleSecretSubmit = useCallback2((value) => {
2863
+ if (!inputRequest.active || !inputRequest.resolve) return;
2864
+ const displayText = inputRequest.isPassword ? "\u2022".repeat(value.length) : value;
2865
+ addMessage(UI_ROLES.SYSTEM, `\u21B3 ${inputRequest.prompt} ${displayText}`);
2866
+ inputRequest.resolve(value);
2867
+ setSecretInput("");
2868
+ }, [inputRequest, addMessage]);
2869
+ useInput((ch, key) => {
2870
+ if (key.escape) {
2871
+ if (inputRequest.active) cancelInputRequest();
2872
+ else if (isProcessing) abort();
2873
+ }
2874
+ if (key.ctrl && ch === "c") handleExit();
2865
2875
  });
2866
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
2867
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginBottom: 1, flexGrow: 1, children: /* @__PURE__ */ jsx2(Static, { items: messages, children: (msg) => {
2868
- const colors = {
2869
- user: THEME.text.accent,
2870
- assistant: THEME.text.primary,
2871
- system: THEME.text.muted,
2872
- error: THEME.status.error,
2873
- tool: THEME.status.running,
2874
- result: THEME.text.muted,
2875
- thinking: THEME.status.warning
2876
- };
2877
- const prefixes = {
2878
- user: "\u276F",
2879
- assistant: ">",
2880
- system: "\u2022",
2881
- error: "\u2717",
2882
- tool: " \u23BF",
2883
- result: " \u2713",
2884
- thinking: "\u{1F9E0}"
2885
- };
2886
- return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: colors[msg.type], dimColor: msg.type === "result", children: [
2887
- prefixes[msg.type],
2888
- " ",
2889
- msg.content
2890
- ] }) }, msg.id);
2891
- } }) }),
2892
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 0, children: [
2893
- thinking.isActive && thinking.content && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
2894
- /* @__PURE__ */ jsx2(Text2, { color: THEME.status.running, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
2895
- /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2896
- " ",
2897
- "Thinking: "
2898
- ] }),
2899
- /* @__PURE__ */ jsx2(Text2, { color: THEME.text.secondary, dimColor: true, children: thinking.content.slice(-200) })
2900
- ] }),
2901
- isProcessing && !thinking.isActive && /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
2902
- /* @__PURE__ */ jsx2(Text2, { color: THEME.spinner, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
2903
- /* @__PURE__ */ jsxs2(Text2, { color: THEME.text.muted, children: [
2904
- " ",
2905
- currentStatus || "Processing"
2906
- ] })
2907
- ] }),
2908
- /* @__PURE__ */ jsx2(
2909
- Box2,
2876
+ useEffect2(() => {
2877
+ const onSignal = () => handleExit();
2878
+ process.on("SIGINT", onSignal);
2879
+ process.on("SIGTERM", onSignal);
2880
+ return () => {
2881
+ process.off("SIGINT", onSignal);
2882
+ process.off("SIGTERM", onSignal);
2883
+ };
2884
+ }, [handleExit]);
2885
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, children: [
2886
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, flexGrow: 1, children: /* @__PURE__ */ jsx5(MessageList, { messages }) }),
2887
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2888
+ /* @__PURE__ */ jsx5(
2889
+ StatusDisplay,
2910
2890
  {
2911
- borderStyle: "single",
2912
- borderColor: THEME.border.default,
2913
- paddingX: 1,
2914
- children: /* @__PURE__ */ jsxs2(Box2, { children: [
2915
- /* @__PURE__ */ jsx2(Text2, { color: THEME.text.secondary, children: "\u25B8" }),
2916
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
2917
- /* @__PURE__ */ jsx2(
2918
- TextInput,
2919
- {
2920
- value: input,
2921
- onChange: setInput,
2922
- onSubmit: handleSubmit,
2923
- placeholder: "Message or /help..."
2924
- }
2925
- )
2926
- ] })
2891
+ reasoning,
2892
+ retryState,
2893
+ isProcessing,
2894
+ currentStatus,
2895
+ elapsedTime,
2896
+ currentTokens
2897
+ }
2898
+ ),
2899
+ /* @__PURE__ */ jsx5(
2900
+ ChatInput,
2901
+ {
2902
+ value: input,
2903
+ onChange: setInput,
2904
+ onSubmit: handleSubmit,
2905
+ placeholder: "Message or /help...",
2906
+ inputRequest,
2907
+ secretInput,
2908
+ setSecretInput,
2909
+ onSecretSubmit: handleSecretSubmit
2927
2910
  }
2928
2911
  ),
2929
- /* @__PURE__ */ jsx2(
2912
+ /* @__PURE__ */ jsx5(
2930
2913
  footer_default,
2931
2914
  {
2932
2915
  phase: stats.phase,
@@ -2956,7 +2939,7 @@ var EXIT_CODE = {
2956
2939
  };
2957
2940
 
2958
2941
  // src/platform/tui/main.tsx
2959
- import { jsx as jsx3 } from "react/jsx-runtime";
2942
+ import { jsx as jsx6 } from "react/jsx-runtime";
2960
2943
  var program = new Command();
2961
2944
  program.name("pentesting").version(APP_VERSION).description(APP_DESCRIPTION).option("--dangerously-skip-permissions", "Skip all permission prompts (dangerous!)").option("-t, --target <target>", "Set initial target");
2962
2945
  program.command("interactive", { isDefault: true }).alias("i").description("Start interactive TUI mode").action(async () => {
@@ -2972,7 +2955,7 @@ program.command("interactive", { isDefault: true }).alias("i").description("Star
2972
2955
  console.log(chalk.hex(THEME.status.error)("[!] All tool executions will be auto-approved!\n"));
2973
2956
  }
2974
2957
  const { waitUntilExit } = render(
2975
- /* @__PURE__ */ jsx3(
2958
+ /* @__PURE__ */ jsx6(
2976
2959
  app_default,
2977
2960
  {
2978
2961
  autoApprove: skipPermissions,
@@ -2991,7 +2974,7 @@ program.command("run <objective>").alias("r").description("Run a single objectiv
2991
2974
  }
2992
2975
  console.log(chalk.hex(THEME.text.accent)(`[target] Objective: ${objective}
2993
2976
  `));
2994
- const agent = new MainAgent();
2977
+ const agent = AgentFactory.createMainAgent(skipPermissions);
2995
2978
  if (skipPermissions) {
2996
2979
  agent.setAutoApprove(true);
2997
2980
  }
@@ -3029,10 +3012,7 @@ program.command("scan <target>").description("Quick scan a target").option("-s,
3029
3012
  console.log(chalk.hex(THEME.text.accent)(`
3030
3013
  [scan] Target: ${target} (${options.scanType})
3031
3014
  `));
3032
- const agent = new MainAgent();
3033
- if (skipPermissions) {
3034
- agent.setAutoApprove(true);
3035
- }
3015
+ const agent = AgentFactory.createMainAgent(skipPermissions);
3036
3016
  agent.addTarget(target);
3037
3017
  agent.setScope([target]);
3038
3018
  const shutdown = async (exitCode = 0) => {