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:
@@ -8,9 +8,21 @@ import { fs, path } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import { PentestError, handlePromptError } from '../error-handling.js';
|
||||
import { MCP_AGENT_MAPPING } from '../constants.js';
|
||||
import type { Authentication, DistributedConfig } from '../types/config.js';
|
||||
|
||||
interface PromptVariables {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
MCP_SERVER?: string;
|
||||
}
|
||||
|
||||
interface IncludeReplacement {
|
||||
placeholder: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// Pure function: Build complete login instructions from config
|
||||
async function buildLoginInstructions(authentication) {
|
||||
async function buildLoginInstructions(authentication: Authentication): Promise<string> {
|
||||
try {
|
||||
// Load the login instructions template
|
||||
const loginInstructionsPath = path.join(import.meta.dirname, '..', '..', 'prompts', 'shared', 'login-instructions.txt');
|
||||
@@ -27,10 +39,10 @@ async function buildLoginInstructions(authentication) {
|
||||
const fullTemplate = await fs.readFile(loginInstructionsPath, 'utf8');
|
||||
|
||||
// Helper function to extract sections based on markers
|
||||
const getSection = (content, sectionName) => {
|
||||
const getSection = (content: string, sectionName: string): string => {
|
||||
const regex = new RegExp(`<!-- BEGIN:${sectionName} -->([\\s\\S]*?)<!-- END:${sectionName} -->`, 'g');
|
||||
const match = regex.exec(content);
|
||||
return match ? match[1].trim() : '';
|
||||
return match ? match[1]!.trim() : '';
|
||||
};
|
||||
|
||||
// Extract sections based on login type
|
||||
@@ -39,7 +51,7 @@ async function buildLoginInstructions(authentication) {
|
||||
|
||||
// Build instructions with only relevant sections
|
||||
const commonSection = getSection(fullTemplate, 'COMMON');
|
||||
const authSection = getSection(fullTemplate, loginType); // FORM or SSO
|
||||
const authSection = loginType ? getSection(fullTemplate, loginType) : ''; // FORM or SSO
|
||||
const verificationSection = getSection(fullTemplate, 'VERIFICATION');
|
||||
|
||||
// Fallback to full template if markers are missing (backward compatibility)
|
||||
@@ -54,7 +66,7 @@ async function buildLoginInstructions(authentication) {
|
||||
}
|
||||
|
||||
// Replace the user instructions placeholder with the login flow from config
|
||||
let userInstructions = authentication.login_flow.join('\n');
|
||||
let userInstructions = (authentication.login_flow ?? []).join('\n');
|
||||
|
||||
// Replace credential placeholders within the user instructions
|
||||
if (authentication.credentials) {
|
||||
@@ -81,22 +93,23 @@ async function buildLoginInstructions(authentication) {
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to build login instructions: ${error.message}`,
|
||||
`Failed to build login instructions: ${errMsg}`,
|
||||
'config',
|
||||
false,
|
||||
{ authentication, originalError: error.message }
|
||||
{ authentication, originalError: errMsg }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function: Process @include() directives
|
||||
async function processIncludes(content, baseDir) {
|
||||
async function processIncludes(content: string, baseDir: string): Promise<string> {
|
||||
const includeRegex = /@include\(([^)]+)\)/g;
|
||||
// Use a Promise.all to handle all includes concurrently
|
||||
const replacements = await Promise.all(
|
||||
const replacements: IncludeReplacement[] = await Promise.all(
|
||||
Array.from(content.matchAll(includeRegex)).map(async (match) => {
|
||||
const includePath = path.join(baseDir, match[1]);
|
||||
const includePath = path.join(baseDir, match[1]!);
|
||||
const sharedContent = await fs.readFile(includePath, 'utf8');
|
||||
return {
|
||||
placeholder: match[0],
|
||||
@@ -112,7 +125,11 @@ async function processIncludes(content, baseDir) {
|
||||
}
|
||||
|
||||
// Pure function: Variable interpolation
|
||||
async function interpolateVariables(template, variables, config = null) {
|
||||
async function interpolateVariables(
|
||||
template: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null = null
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (!template || typeof template !== 'string') {
|
||||
throw new PentestError(
|
||||
@@ -147,8 +164,8 @@ async function interpolateVariables(template, variables, config = null) {
|
||||
const cleanRulesSection = '<rules>\nNo specific rules or focus areas provided for this test.\n</rules>';
|
||||
result = result.replace(/<rules>[\s\S]*?<\/rules>/g, cleanRulesSection);
|
||||
} else {
|
||||
const avoidRules = hasAvoidRules ? config.avoid.map(r => `- ${r.description}`).join('\n') : 'None';
|
||||
const focusRules = hasFocusRules ? config.focus.map(r => `- ${r.description}`).join('\n') : 'None';
|
||||
const avoidRules = hasAvoidRules ? config.avoid!.map(r => `- ${r.description}`).join('\n') : 'None';
|
||||
const focusRules = hasFocusRules ? config.focus!.map(r => `- ${r.description}`).join('\n') : 'None';
|
||||
|
||||
result = result
|
||||
.replace(/{{RULES_AVOID}}/g, avoidRules)
|
||||
@@ -180,17 +197,23 @@ async function interpolateVariables(template, variables, config = null) {
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Variable interpolation failed: ${error.message}`,
|
||||
`Variable interpolation failed: ${errMsg}`,
|
||||
'prompt',
|
||||
false,
|
||||
{ originalError: error.message }
|
||||
{ originalError: errMsg }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function: Load and interpolate prompt template
|
||||
export async function loadPrompt(promptName, variables, config = null, pipelineTestingMode = false) {
|
||||
export async function loadPrompt(
|
||||
promptName: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null = null,
|
||||
pipelineTestingMode: boolean = false
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Use pipeline testing prompts if pipeline testing mode is enabled
|
||||
const baseDir = pipelineTestingMode ? 'prompts/pipeline-testing' : 'prompts';
|
||||
@@ -213,11 +236,12 @@ export async function loadPrompt(promptName, variables, config = null, pipelineT
|
||||
}
|
||||
|
||||
// Add MCP server assignment to variables
|
||||
const enhancedVariables = { ...variables };
|
||||
const enhancedVariables: PromptVariables = { ...variables };
|
||||
|
||||
// Assign MCP server based on prompt name (agent name)
|
||||
if (MCP_AGENT_MAPPING[promptName]) {
|
||||
enhancedVariables.MCP_SERVER = MCP_AGENT_MAPPING[promptName];
|
||||
const mcpServer = MCP_AGENT_MAPPING[promptName as keyof typeof MCP_AGENT_MAPPING];
|
||||
if (mcpServer) {
|
||||
enhancedVariables.MCP_SERVER = mcpServer;
|
||||
console.log(chalk.gray(` 🎭 Assigned ${promptName} → ${enhancedVariables.MCP_SERVER}`));
|
||||
} else {
|
||||
// Fallback for unknown agents
|
||||
@@ -235,7 +259,7 @@ export async function loadPrompt(promptName, variables, config = null, pipelineT
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
const promptError = handlePromptError(promptName, error);
|
||||
const promptError = handlePromptError(promptName, error as Error);
|
||||
throw promptError.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user