mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
156 lines
3.8 KiB
TypeScript
156 lines
3.8 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2026 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|