| |
| |
| |
| |
| |
|
|
| import { SHELL_TOOL_NAME } from '../tools/definitions/base-declarations.js'; |
|
|
| const WORKFLOW_PART_SEPARATOR = ' | '; |
| const TOOL_SEQUENCE_SEPARATOR = ' -> '; |
| const SHELL_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=/; |
| const SAFE_COMMAND_NAME_REGEX = /^[A-Za-z0-9_.@+-]+$/; |
| const SAFE_TOOL_SEQUENCE_ENTRY_REGEX = /^[A-Za-z_][A-Za-z0-9_:.]*$/; |
|
|
| function tokenizeShellCommand(command: string): string[] { |
| const tokens: string[] = []; |
| let currentToken = ''; |
| let quote: '"' | "'" | '`' | undefined; |
|
|
| for (let i = 0; i < command.length; i++) { |
| const char = command[i]; |
|
|
| if (quote) { |
| if (char === quote) { |
| quote = undefined; |
| continue; |
| } |
|
|
| if (quote === '"' && char === '\\' && i + 1 < command.length) { |
| currentToken += command[i + 1]; |
| i++; |
| continue; |
| } |
|
|
| currentToken += char; |
| continue; |
| } |
|
|
| if (char === ' ' || char === '\t' || char === '\n' || char === '\r') { |
| if (currentToken) { |
| tokens.push(currentToken); |
| currentToken = ''; |
| } |
| continue; |
| } |
|
|
| if (char === '"' || char === "'" || char === '`') { |
| quote = char; |
| continue; |
| } |
|
|
| currentToken += char; |
| } |
|
|
| if (currentToken) { |
| tokens.push(currentToken); |
| } |
|
|
| return tokens; |
| } |
|
|
| function getSafeCommandName(token: string): string | undefined { |
| if (!token || SHELL_ASSIGNMENT_REGEX.test(token)) { |
| return undefined; |
| } |
|
|
| const pathParts = token.split(/[/\\]/).filter(Boolean); |
| const basename = pathParts[pathParts.length - 1] ?? token; |
| if (!basename || basename.includes('://')) { |
| return 'shell'; |
| } |
|
|
| return SAFE_COMMAND_NAME_REGEX.test(basename) ? basename : 'shell'; |
| } |
|
|
| export function summarizeShellCommandForScratchpad( |
| command: string, |
| ): string | undefined { |
| const normalized = command.replace(/\s+/g, ' ').trim(); |
| if (normalized.length === 0) { |
| return undefined; |
| } |
|
|
| for (const token of tokenizeShellCommand(normalized)) { |
| const commandName = getSafeCommandName(token); |
| if (commandName) { |
| return commandName; |
| } |
| } |
|
|
| return undefined; |
| } |
|
|
| function sanitizeWorkflowToolSequenceEntry(entry: string): string | undefined { |
| const trimmed = entry.trim(); |
| if (!trimmed) { |
| return undefined; |
| } |
|
|
| const shellPrefix = `${SHELL_TOOL_NAME}:`; |
| if (trimmed.startsWith(shellPrefix)) { |
| const command = trimmed.slice(shellPrefix.length).trim(); |
| const commandSummary = summarizeShellCommandForScratchpad(command); |
| return commandSummary |
| ? `${SHELL_TOOL_NAME}: ${commandSummary}` |
| : SHELL_TOOL_NAME; |
| } |
|
|
| if ( |
| trimmed === SHELL_TOOL_NAME || |
| SAFE_TOOL_SEQUENCE_ENTRY_REGEX.test(trimmed) |
| ) { |
| return trimmed; |
| } |
|
|
| return undefined; |
| } |
|
|
| export function sanitizeWorkflowSummaryForScratchpad(summary: string): string { |
| const normalized = summary.replace(/\s+/g, ' ').trim(); |
| if (!normalized.includes(`${SHELL_TOOL_NAME}:`)) { |
| return normalized; |
| } |
|
|
| const sanitizedParts: string[] = []; |
| for (const part of normalized.split(WORKFLOW_PART_SEPARATOR)) { |
| const trimmed = part.trim(); |
| if (!trimmed) { |
| continue; |
| } |
|
|
| if (trimmed.includes(`${SHELL_TOOL_NAME}:`)) { |
| const sanitizedToolSequence = trimmed |
| .split(TOOL_SEQUENCE_SEPARATOR) |
| .map(sanitizeWorkflowToolSequenceEntry) |
| .filter((entry): entry is string => Boolean(entry)); |
| if (sanitizedToolSequence.length > 0) { |
| sanitizedParts.push( |
| sanitizedToolSequence.join(TOOL_SEQUENCE_SEPARATOR), |
| ); |
| } |
| continue; |
| } |
|
|
| if ( |
| trimmed.startsWith('paths ') || |
| trimmed === 'validated' || |
| trimmed === 'validation failed' |
| ) { |
| sanitizedParts.push(trimmed); |
| } |
| } |
|
|
| return sanitizedParts.join(WORKFLOW_PART_SEPARATOR); |
| } |
|
|