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 7d91373fdb
commit 3ac07a4718
55 changed files with 3213 additions and 2057 deletions
@@ -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;
}
}
}