feat: typescript migration (#40)

* chore: initialize TypeScript configuration and build setup

- Add tsconfig.json for root and mcp-server with strict type checking
- Install typescript and @types/node as devDependencies
- Add npm build script for TypeScript compilation
- Update main entrypoint to compiled dist/shannon.js
- Update Dockerfile to build TypeScript before running
- Configure output directory and module resolution for Node.js

* refactor: migrate codebase from JavaScript to TypeScript

- Convert all 37 JavaScript files to TypeScript (.js -> .ts)
- Add type definitions in src/types/ for agents, config, errors, session
- Update mcp-server with proper TypeScript types
- Move entry point from shannon.mjs to src/shannon.ts
- Update tsconfig.json with rootDir: "./src" for cleaner dist output
- Update Dockerfile to build TypeScript before runtime
- Update package.json paths to use compiled dist/shannon.js

No runtime behavior changes - pure type safety migration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update CLI references from ./shannon.mjs to shannon

- Update help text in src/cli/ui.ts
- Update usage examples in src/cli/command-handler.ts
- Update setup message in src/shannon.ts
- Update CLAUDE.md documentation with TypeScript file structure
- Replace all ./shannon.mjs references with shannon command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove unnecessary eslint-disable comments

ESLint is not configured in this project, making these comments redundant.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ezl-keygraph
2026-01-08 00:18:25 +05:30
committed by GitHub
parent b4d2c35b91
commit dd18f4629b
55 changed files with 3213 additions and 2057 deletions
@@ -11,6 +11,8 @@
* concurrent session operations.
*/
type UnlockFunction = () => void;
/**
* SessionMutex - Promise-based mutex for session file operations
*
@@ -19,7 +21,7 @@
* during parallel execution of vulnerability analysis and exploitation phases.
*
* Usage:
* ```js
* ```ts
* const mutex = new SessionMutex();
* const unlock = await mutex.lock(sessionId);
* try {
@@ -30,31 +32,27 @@
* ```
*/
export class SessionMutex {
constructor() {
// Map of sessionId -> Promise (represents active lock)
this.locks = new Map();
}
// Map of sessionId -> Promise (represents active lock)
private locks: Map<string, Promise<void>> = new Map();
/**
* Acquire lock for a session
* @param {string} sessionId - Session ID to lock
* @returns {Promise<Function>} Unlock function to release the lock
*/
async lock(sessionId) {
async lock(sessionId: string): Promise<UnlockFunction> {
if (this.locks.has(sessionId)) {
// Wait for existing lock to be released
await this.locks.get(sessionId);
}
// Create new lock promise
let resolve;
const promise = new Promise(r => resolve = r);
let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
this.locks.set(sessionId, promise);
// Return unlock function
return () => {
this.locks.delete(sessionId);
resolve();
resolve!();
};
}
}
-201
View File
@@ -1,201 +0,0 @@
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
import { $ } from 'zx';
import chalk from 'chalk';
// Global git operations semaphore to prevent index.lock conflicts during parallel execution
class GitSemaphore {
constructor() {
this.queue = [];
this.running = false;
}
async acquire() {
return new Promise((resolve) => {
this.queue.push(resolve);
this.process();
});
}
release() {
this.running = false;
this.process();
}
process() {
if (!this.running && this.queue.length > 0) {
this.running = true;
const resolve = this.queue.shift();
resolve();
}
}
}
const gitSemaphore = new GitSemaphore();
// Execute git commands with retry logic for index.lock conflicts
export const executeGitCommandWithRetry = async (commandArgs, sourceDir, description, maxRetries = 5) => {
await gitSemaphore.acquire();
try {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Handle both array and string commands
let result;
if (Array.isArray(commandArgs)) {
// For arrays like ['git', 'status', '--porcelain'], execute parts separately
const [cmd, ...args] = commandArgs;
result = await $`cd ${sourceDir} && ${cmd} ${args}`;
} else {
// For string commands
result = await $`cd ${sourceDir} && ${commandArgs}`;
}
return result;
} catch (error) {
const isLockError = error.message.includes('index.lock') ||
error.message.includes('unable to lock') ||
error.message.includes('Another git process') ||
error.message.includes('fatal: Unable to create') ||
error.message.includes('fatal: index file');
if (isLockError && attempt < maxRetries) {
const delay = Math.pow(2, attempt - 1) * 1000; // Exponential backoff: 1s, 2s, 4s, 8s, 16s
console.log(chalk.yellow(` ⚠️ Git lock conflict during ${description} (attempt ${attempt}/${maxRetries}). Retrying in ${delay}ms...`));
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
} finally {
gitSemaphore.release();
}
};
// Pure functions for Git workspace management
const cleanWorkspace = async (sourceDir, reason = 'clean start') => {
console.log(chalk.blue(` 🧹 Cleaning workspace for ${reason}`));
try {
// Check for uncommitted changes
const status = await $`cd ${sourceDir} && git status --porcelain`;
const hasChanges = status.stdout.trim().length > 0;
if (hasChanges) {
// Show what we're about to remove
const changes = status.stdout.trim().split('\n').filter(line => line.length > 0);
console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`));
await $`cd ${sourceDir} && git reset --hard HEAD`;
await $`cd ${sourceDir} && git clean -fd`;
console.log(chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`));
changes.slice(0, 3).forEach(change => console.log(chalk.gray(` ${change}`)));
if (changes.length > 3) {
console.log(chalk.gray(` ... and ${changes.length - 3} more files`));
}
} else {
console.log(chalk.blue(` ✅ Workspace already clean (no changes to remove)`));
}
return { success: true, hadChanges: hasChanges };
} catch (error) {
console.log(chalk.yellow(` ⚠️ Workspace cleanup failed: ${error.message}`));
return { success: false, error };
}
};
export const createGitCheckpoint = async (sourceDir, description, attempt) => {
console.log(chalk.blue(` 📍 Creating checkpoint for ${description} (attempt ${attempt})`));
try {
// Only clean workspace on retry attempts (attempt > 1), not on first attempts
// This preserves deliverables between agents while still cleaning on actual retries
if (attempt > 1) {
const cleanResult = await cleanWorkspace(sourceDir, `${description} (retry cleanup)`);
if (!cleanResult.success) {
console.log(chalk.yellow(` ⚠️ Workspace cleanup failed, continuing anyway: ${cleanResult.error.message}`));
}
}
// Check for uncommitted changes with retry logic
const status = await executeGitCommandWithRetry(['git', 'status', '--porcelain'], sourceDir, 'status check');
const hasChanges = status.stdout.trim().length > 0;
// Stage changes with retry logic
await executeGitCommandWithRetry(['git', 'add', '-A'], sourceDir, 'staging changes');
// Create commit with retry logic
await executeGitCommandWithRetry(['git', 'commit', '-m', `📍 Checkpoint: ${description} (attempt ${attempt})`, '--allow-empty'], sourceDir, 'creating commit');
if (hasChanges) {
console.log(chalk.blue(` ✅ Checkpoint created with uncommitted changes staged`));
} else {
console.log(chalk.blue(` ✅ Empty checkpoint created (no workspace changes)`));
}
return { success: true };
} catch (error) {
console.log(chalk.yellow(` ⚠️ Checkpoint creation failed after retries: ${error.message}`));
return { success: false, error };
}
};
export const commitGitSuccess = async (sourceDir, description) => {
console.log(chalk.green(` 💾 Committing successful results for ${description}`));
try {
// Check what we're about to commit with retry logic
const status = await executeGitCommandWithRetry(['git', 'status', '--porcelain'], sourceDir, 'status check for success commit');
const changes = status.stdout.trim().split('\n').filter(line => line.length > 0);
// Stage changes with retry logic
await executeGitCommandWithRetry(['git', 'add', '-A'], sourceDir, 'staging changes for success commit');
// Create success commit with retry logic
await executeGitCommandWithRetry(['git', 'commit', '-m', `${description}: completed successfully`, '--allow-empty'], sourceDir, 'creating success commit');
if (changes.length > 0) {
console.log(chalk.green(` ✅ Success commit created with ${changes.length} file changes:`));
changes.slice(0, 5).forEach(change => console.log(chalk.gray(` ${change}`)));
if (changes.length > 5) {
console.log(chalk.gray(` ... and ${changes.length - 5} more files`));
}
} else {
console.log(chalk.green(` ✅ Empty success commit created (agent made no file changes)`));
}
return { success: true };
} catch (error) {
console.log(chalk.yellow(` ⚠️ Success commit failed after retries: ${error.message}`));
return { success: false, error };
}
};
export const rollbackGitWorkspace = async (sourceDir, reason = 'retry preparation') => {
console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`));
try {
// Show what we're about to remove with retry logic
const status = await executeGitCommandWithRetry(['git', 'status', '--porcelain'], sourceDir, 'status check for rollback');
const changes = status.stdout.trim().split('\n').filter(line => line.length > 0);
// Reset to HEAD with retry logic
await executeGitCommandWithRetry(['git', 'reset', '--hard', 'HEAD'], sourceDir, 'hard reset for rollback');
// Clean untracked files with retry logic
await executeGitCommandWithRetry(['git', 'clean', '-fd'], sourceDir, 'cleaning untracked files for rollback');
if (changes.length > 0) {
console.log(chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`));
changes.slice(0, 3).forEach(change => console.log(chalk.gray(` ${change}`)));
if (changes.length > 3) {
console.log(chalk.gray(` ... and ${changes.length - 3} more files`));
}
} else {
console.log(chalk.yellow(` ✅ Rollback completed - no changes to remove`));
}
return { success: true };
} catch (error) {
console.log(chalk.red(` ❌ Rollback failed after retries: ${error.message}`));
return { success: false, error };
}
};
+276
View File
@@ -0,0 +1,276 @@
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
import { $ } from 'zx';
import chalk from 'chalk';
interface GitOperationResult {
success: boolean;
hadChanges?: boolean;
error?: Error;
}
// Global git operations semaphore to prevent index.lock conflicts during parallel execution
class GitSemaphore {
private queue: Array<() => void> = [];
private running: boolean = false;
async acquire(): Promise<void> {
return new Promise((resolve) => {
this.queue.push(resolve);
this.process();
});
}
release(): void {
this.running = false;
this.process();
}
private process(): void {
if (!this.running && this.queue.length > 0) {
this.running = true;
const resolve = this.queue.shift();
resolve!();
}
}
}
const gitSemaphore = new GitSemaphore();
// Execute git commands with retry logic for index.lock conflicts
export const executeGitCommandWithRetry = async (
commandArgs: string[],
sourceDir: string,
description: string,
maxRetries: number = 5
): Promise<{ stdout: string; stderr: string }> => {
await gitSemaphore.acquire();
try {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// For arrays like ['git', 'status', '--porcelain'], execute parts separately
const [cmd, ...args] = commandArgs;
const result = await $`cd ${sourceDir} && ${cmd} ${args}`;
return result;
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
const isLockError =
errMsg.includes('index.lock') ||
errMsg.includes('unable to lock') ||
errMsg.includes('Another git process') ||
errMsg.includes('fatal: Unable to create') ||
errMsg.includes('fatal: index file');
if (isLockError && attempt < maxRetries) {
const delay = Math.pow(2, attempt - 1) * 1000; // Exponential backoff: 1s, 2s, 4s, 8s, 16s
console.log(
chalk.yellow(
` ⚠️ Git lock conflict during ${description} (attempt ${attempt}/${maxRetries}). Retrying in ${delay}ms...`
)
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
// Should never reach here but TypeScript needs a return
throw new Error(`Git command failed after ${maxRetries} retries`);
} finally {
gitSemaphore.release();
}
};
// Pure functions for Git workspace management
const cleanWorkspace = async (
sourceDir: string,
reason: string = 'clean start'
): Promise<GitOperationResult> => {
console.log(chalk.blue(` 🧹 Cleaning workspace for ${reason}`));
try {
// Check for uncommitted changes
const status = await $`cd ${sourceDir} && git status --porcelain`;
const hasChanges = status.stdout.trim().length > 0;
if (hasChanges) {
// Show what we're about to remove
const changes = status.stdout
.trim()
.split('\n')
.filter((line) => line.length > 0);
console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`));
await $`cd ${sourceDir} && git reset --hard HEAD`;
await $`cd ${sourceDir} && git clean -fd`;
console.log(
chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`)
);
changes.slice(0, 3).forEach((change) => console.log(chalk.gray(` ${change}`)));
if (changes.length > 3) {
console.log(chalk.gray(` ... and ${changes.length - 3} more files`));
}
} else {
console.log(chalk.blue(` ✅ Workspace already clean (no changes to remove)`));
}
return { success: true, hadChanges: hasChanges };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
console.log(chalk.yellow(` ⚠️ Workspace cleanup failed: ${errMsg}`));
return { success: false, error: error instanceof Error ? error : new Error(errMsg) };
}
};
export const createGitCheckpoint = async (
sourceDir: string,
description: string,
attempt: number
): Promise<GitOperationResult> => {
console.log(chalk.blue(` 📍 Creating checkpoint for ${description} (attempt ${attempt})`));
try {
// Only clean workspace on retry attempts (attempt > 1), not on first attempts
// This preserves deliverables between agents while still cleaning on actual retries
if (attempt > 1) {
const cleanResult = await cleanWorkspace(sourceDir, `${description} (retry cleanup)`);
if (!cleanResult.success) {
const errMsg = cleanResult.error?.message || 'Unknown error';
console.log(
chalk.yellow(` ⚠️ Workspace cleanup failed, continuing anyway: ${errMsg}`)
);
}
}
// Check for uncommitted changes with retry logic
const status = await executeGitCommandWithRetry(
['git', 'status', '--porcelain'],
sourceDir,
'status check'
);
const hasChanges = status.stdout.trim().length > 0;
// Stage changes with retry logic
await executeGitCommandWithRetry(['git', 'add', '-A'], sourceDir, 'staging changes');
// Create commit with retry logic
await executeGitCommandWithRetry(
['git', 'commit', '-m', `📍 Checkpoint: ${description} (attempt ${attempt})`, '--allow-empty'],
sourceDir,
'creating commit'
);
if (hasChanges) {
console.log(chalk.blue(` ✅ Checkpoint created with uncommitted changes staged`));
} else {
console.log(chalk.blue(` ✅ Empty checkpoint created (no workspace changes)`));
}
return { success: true };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
console.log(chalk.yellow(` ⚠️ Checkpoint creation failed after retries: ${errMsg}`));
return { success: false, error: error instanceof Error ? error : new Error(errMsg) };
}
};
export const commitGitSuccess = async (
sourceDir: string,
description: string
): Promise<GitOperationResult> => {
console.log(chalk.green(` 💾 Committing successful results for ${description}`));
try {
// Check what we're about to commit with retry logic
const status = await executeGitCommandWithRetry(
['git', 'status', '--porcelain'],
sourceDir,
'status check for success commit'
);
const changes = status.stdout
.trim()
.split('\n')
.filter((line) => line.length > 0);
// Stage changes with retry logic
await executeGitCommandWithRetry(
['git', 'add', '-A'],
sourceDir,
'staging changes for success commit'
);
// Create success commit with retry logic
await executeGitCommandWithRetry(
['git', 'commit', '-m', `${description}: completed successfully`, '--allow-empty'],
sourceDir,
'creating success commit'
);
if (changes.length > 0) {
console.log(chalk.green(` ✅ Success commit created with ${changes.length} file changes:`));
changes.slice(0, 5).forEach((change) => console.log(chalk.gray(` ${change}`)));
if (changes.length > 5) {
console.log(chalk.gray(` ... and ${changes.length - 5} more files`));
}
} else {
console.log(chalk.green(` ✅ Empty success commit created (agent made no file changes)`));
}
return { success: true };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
console.log(chalk.yellow(` ⚠️ Success commit failed after retries: ${errMsg}`));
return { success: false, error: error instanceof Error ? error : new Error(errMsg) };
}
};
export const rollbackGitWorkspace = async (
sourceDir: string,
reason: string = 'retry preparation'
): Promise<GitOperationResult> => {
console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`));
try {
// Show what we're about to remove with retry logic
const status = await executeGitCommandWithRetry(
['git', 'status', '--porcelain'],
sourceDir,
'status check for rollback'
);
const changes = status.stdout
.trim()
.split('\n')
.filter((line) => line.length > 0);
// Reset to HEAD with retry logic
await executeGitCommandWithRetry(
['git', 'reset', '--hard', 'HEAD'],
sourceDir,
'hard reset for rollback'
);
// Clean untracked files with retry logic
await executeGitCommandWithRetry(
['git', 'clean', '-fd'],
sourceDir,
'cleaning untracked files for rollback'
);
if (changes.length > 0) {
console.log(
chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`)
);
changes.slice(0, 3).forEach((change) => console.log(chalk.gray(` ${change}`)));
if (changes.length > 3) {
console.log(chalk.gray(` ... and ${changes.length - 3} more files`));
}
} else {
console.log(chalk.yellow(` ✅ Rollback completed - no changes to remove`));
}
return { success: true };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
console.log(chalk.red(` ❌ Rollback failed after retries: ${errMsg}`));
return { success: false, error: error instanceof Error ? error : new Error(errMsg) };
}
};
+72 -16
View File
@@ -10,38 +10,74 @@ import { formatDuration } from '../audit/utils.js';
// Timing utilities
export class Timer {
constructor(name) {
name: string;
startTime: number;
endTime: number | null = null;
constructor(name: string) {
this.name = name;
this.startTime = Date.now();
this.endTime = null;
}
stop() {
stop(): number {
this.endTime = Date.now();
return this.duration();
}
duration() {
duration(): number {
const end = this.endTime || Date.now();
return end - this.startTime;
}
}
interface TimingResultsPhases {
[key: string]: number;
}
interface TimingResultsCommands {
[key: string]: number;
}
interface TimingResultsAgents {
[key: string]: number;
}
interface TimingResults {
total: Timer | null;
phases: TimingResultsPhases;
commands: TimingResultsCommands;
agents: TimingResultsAgents;
}
interface CostResultsAgents {
[key: string]: number;
}
interface CostResults {
agents: CostResultsAgents;
total: number;
}
// Global timing and cost tracker
export const timingResults = {
export const timingResults: TimingResults = {
total: null,
phases: {},
commands: {},
agents: {}
agents: {},
};
export const costResults = {
export const costResults: CostResults = {
agents: {},
total: 0
total: 0,
};
// Function to display comprehensive timing summary
export const displayTimingSummary = () => {
export const displayTimingSummary = (): void => {
if (!timingResults.total) {
console.log(chalk.yellow('No timing data available'));
return;
}
const totalDuration = timingResults.total.stop();
console.log(chalk.cyan.bold('\n⏱️ TIMING SUMMARY'));
@@ -57,10 +93,16 @@ export const displayTimingSummary = () => {
let phaseTotal = 0;
for (const [phase, duration] of Object.entries(timingResults.phases)) {
const percentage = ((duration / totalDuration) * 100).toFixed(1);
console.log(chalk.yellow(` ${phase.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`));
console.log(
chalk.yellow(` ${phase.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`)
);
phaseTotal += duration;
}
console.log(chalk.gray(` ${'Phases Total'.padEnd(20)} ${formatDuration(phaseTotal).padStart(8)} (${((phaseTotal / totalDuration) * 100).toFixed(1)}%)`));
console.log(
chalk.gray(
` ${'Phases Total'.padEnd(20)} ${formatDuration(phaseTotal).padStart(8)} (${((phaseTotal / totalDuration) * 100).toFixed(1)}%)`
)
);
console.log();
}
@@ -70,10 +112,16 @@ export const displayTimingSummary = () => {
let commandTotal = 0;
for (const [command, duration] of Object.entries(timingResults.commands)) {
const percentage = ((duration / totalDuration) * 100).toFixed(1);
console.log(chalk.blue(` ${command.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`));
console.log(
chalk.blue(` ${command.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`)
);
commandTotal += duration;
}
console.log(chalk.gray(` ${'Commands Total'.padEnd(20)} ${formatDuration(commandTotal).padStart(8)} (${((commandTotal / totalDuration) * 100).toFixed(1)}%)`));
console.log(
chalk.gray(
` ${'Commands Total'.padEnd(20)} ${formatDuration(commandTotal).padStart(8)} (${((commandTotal / totalDuration) * 100).toFixed(1)}%)`
)
);
console.log();
}
@@ -84,10 +132,18 @@ export const displayTimingSummary = () => {
for (const [agent, duration] of Object.entries(timingResults.agents)) {
const percentage = ((duration / totalDuration) * 100).toFixed(1);
const displayName = agent.replace(/-/g, ' ');
console.log(chalk.magenta(` ${displayName.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`));
console.log(
chalk.magenta(
` ${displayName.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`
)
);
agentTotal += duration;
}
console.log(chalk.gray(` ${'Agents Total'.padEnd(20)} ${formatDuration(agentTotal).padStart(8)} (${((agentTotal / totalDuration) * 100).toFixed(1)}%)`));
console.log(
chalk.gray(
` ${'Agents Total'.padEnd(20)} ${formatDuration(agentTotal).padStart(8)} (${((agentTotal / totalDuration) * 100).toFixed(1)}%)`
)
);
}
// Cost breakdown
@@ -101,4 +157,4 @@ export const displayTimingSummary = () => {
}
console.log(chalk.gray('─'.repeat(60)));
};
};
@@ -6,10 +6,30 @@
import { AGENTS } from '../session-manager.js';
interface ToolCallInput {
url?: string;
element?: string;
key?: string;
fields?: unknown[];
text?: string;
action?: string;
description?: string;
todos?: Array<{
status: string;
content: string;
}>;
[key: string]: unknown;
}
interface ToolCall {
name: string;
input?: ToolCallInput;
}
/**
* Extract domain from URL for display
*/
function extractDomain(url) {
function extractDomain(url: string): string {
try {
const urlObj = new URL(url);
return urlObj.hostname || url.slice(0, 30);
@@ -21,24 +41,24 @@ function extractDomain(url) {
/**
* Summarize TodoWrite updates into clean progress indicators
*/
function summarizeTodoUpdate(input) {
function summarizeTodoUpdate(input: ToolCallInput | undefined): string | null {
if (!input?.todos || !Array.isArray(input.todos)) {
return null;
}
const todos = input.todos;
const completed = todos.filter(t => t.status === 'completed');
const inProgress = todos.filter(t => t.status === 'in_progress');
const completed = todos.filter((t) => t.status === 'completed');
const inProgress = todos.filter((t) => t.status === 'in_progress');
// Show recently completed tasks
if (completed.length > 0) {
const recent = completed[completed.length - 1];
const recent = completed[completed.length - 1]!;
return `${recent.content}`;
}
// Show current in-progress task
if (inProgress.length > 0) {
const current = inProgress[0];
const current = inProgress[0]!;
return `🔄 ${current.content}`;
}
@@ -48,9 +68,9 @@ function summarizeTodoUpdate(input) {
/**
* Get agent prefix for parallel execution
*/
export function getAgentPrefix(description) {
export function getAgentPrefix(description: string): string {
// Map agent names to their prefixes
const agentPrefixes = {
const agentPrefixes: Record<string, string> = {
'injection-vuln': '[Injection]',
'xss-vuln': '[XSS]',
'auth-vuln': '[Auth]',
@@ -60,12 +80,13 @@ export function getAgentPrefix(description) {
'xss-exploit': '[XSS]',
'auth-exploit': '[Auth]',
'authz-exploit': '[Authz]',
'ssrf-exploit': '[SSRF]'
'ssrf-exploit': '[SSRF]',
};
// First try to match by agent name directly
for (const [agentName, prefix] of Object.entries(agentPrefixes)) {
if (AGENTS[agentName] && description.includes(AGENTS[agentName].displayName)) {
const agent = AGENTS[agentName as keyof typeof AGENTS];
if (agent && description.includes(agent.displayName)) {
return prefix;
}
}
@@ -73,7 +94,7 @@ export function getAgentPrefix(description) {
// Fallback to partial matches for backwards compatibility
if (description.includes('injection')) return '[Injection]';
if (description.includes('xss')) return '[XSS]';
if (description.includes('authz')) return '[Authz]'; // Check authz before auth
if (description.includes('authz')) return '[Authz]'; // Check authz before auth
if (description.includes('auth')) return '[Auth]';
if (description.includes('ssrf')) return '[SSRF]';
@@ -83,7 +104,7 @@ export function getAgentPrefix(description) {
/**
* Format browser tool calls into clean progress indicators
*/
function formatBrowserAction(toolCall) {
function formatBrowserAction(toolCall: ToolCall): string {
const toolName = toolCall.name;
const input = toolCall.input || {};
@@ -181,13 +202,13 @@ function formatBrowserAction(toolCall) {
/**
* Filter out JSON tool calls from content, with special handling for Task calls
*/
export function filterJsonToolCalls(content) {
export function filterJsonToolCalls(content: string | null | undefined): string {
if (!content || typeof content !== 'string') {
return content;
return content || '';
}
const lines = content.split('\n');
const processedLines = [];
const processedLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
@@ -200,7 +221,7 @@ export function filterJsonToolCalls(content) {
// Check if this is a JSON tool call
if (trimmed.startsWith('{"type":"tool_use"')) {
try {
const toolCall = JSON.parse(trimmed);
const toolCall = JSON.parse(trimmed) as ToolCall;
// Special handling for Task tool calls
if (toolCall.name === 'Task') {
@@ -229,8 +250,7 @@ export function filterJsonToolCalls(content) {
// Hide all other tool calls (Read, Write, Grep, etc.)
continue;
} catch (error) {
} catch {
// If JSON parsing fails, treat as regular text
processedLines.push(line);
}
@@ -241,4 +261,4 @@ export function filterJsonToolCalls(content) {
}
return processedLines.join('\n');
}
}