mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
feat(memory): persist auto-memory scratchpad for skill extraction (#25873)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
Reference in New Issue
Block a user