@within-7/minto 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/AgentsCommand.js +22 -24
- package/dist/commands/agents/AgentsCommand.js.map +2 -2
- package/dist/commands/context.js +2 -1
- package/dist/commands/context.js.map +2 -2
- package/dist/commands/export.js +2 -1
- package/dist/commands/export.js.map +2 -2
- package/dist/commands/mcp-interactive.js +7 -6
- package/dist/commands/mcp-interactive.js.map +2 -2
- package/dist/commands/model.js +3 -2
- package/dist/commands/model.js.map +2 -2
- package/dist/commands/permissions.js +4 -3
- package/dist/commands/permissions.js.map +2 -2
- package/dist/commands/plugin/AddMarketplaceForm.js +3 -2
- package/dist/commands/plugin/AddMarketplaceForm.js.map +2 -2
- package/dist/commands/plugin/ConfirmDialog.js +2 -1
- package/dist/commands/plugin/ConfirmDialog.js.map +2 -2
- package/dist/commands/plugin/ErrorView.js +2 -1
- package/dist/commands/plugin/ErrorView.js.map +2 -2
- package/dist/commands/plugin/InstalledPluginsByMarketplace.js +5 -4
- package/dist/commands/plugin/InstalledPluginsByMarketplace.js.map +2 -2
- package/dist/commands/plugin/InstalledPluginsManager.js +5 -4
- package/dist/commands/plugin/InstalledPluginsManager.js.map +2 -2
- package/dist/commands/plugin/MainMenu.js +2 -1
- package/dist/commands/plugin/MainMenu.js.map +2 -2
- package/dist/commands/plugin/MarketplaceManager.js +5 -4
- package/dist/commands/plugin/MarketplaceManager.js.map +2 -2
- package/dist/commands/plugin/MarketplaceSelector.js +4 -3
- package/dist/commands/plugin/MarketplaceSelector.js.map +2 -2
- package/dist/commands/plugin/PlaceholderScreen.js +3 -2
- package/dist/commands/plugin/PlaceholderScreen.js.map +2 -2
- package/dist/commands/plugin/PluginBrowser.js +6 -5
- package/dist/commands/plugin/PluginBrowser.js.map +2 -2
- package/dist/commands/plugin/PluginDetailsInstall.js +5 -4
- package/dist/commands/plugin/PluginDetailsInstall.js.map +2 -2
- package/dist/commands/plugin/PluginDetailsManage.js +4 -3
- package/dist/commands/plugin/PluginDetailsManage.js.map +2 -2
- package/dist/commands/plugin.js +16 -15
- package/dist/commands/plugin.js.map +2 -2
- package/dist/commands/sandbox.js +4 -3
- package/dist/commands/sandbox.js.map +2 -2
- package/dist/commands/setup.js +2 -1
- package/dist/commands/setup.js.map +2 -2
- package/dist/commands/status.js +2 -1
- package/dist/commands/status.js.map +2 -2
- package/dist/commands/undo.js +245 -0
- package/dist/commands/undo.js.map +7 -0
- package/dist/commands.js +2 -0
- package/dist/commands.js.map +2 -2
- package/dist/components/AgentThinkingBlock.js +1 -1
- package/dist/components/AgentThinkingBlock.js.map +2 -2
- package/dist/components/AsciiLogo.js +7 -8
- package/dist/components/AsciiLogo.js.map +2 -2
- package/dist/components/AskUserQuestionDialog/AskUserQuestionDialog.js +3 -2
- package/dist/components/AskUserQuestionDialog/AskUserQuestionDialog.js.map +2 -2
- package/dist/components/AskUserQuestionDialog/QuestionView.js +2 -1
- package/dist/components/AskUserQuestionDialog/QuestionView.js.map +2 -2
- package/dist/components/CollapsibleHint.js +2 -1
- package/dist/components/CollapsibleHint.js.map +2 -2
- package/dist/components/Config.js +3 -2
- package/dist/components/Config.js.map +2 -2
- package/dist/components/ConsoleOAuthFlow.js +2 -1
- package/dist/components/ConsoleOAuthFlow.js.map +2 -2
- package/dist/components/Cost.js +2 -1
- package/dist/components/Cost.js.map +2 -2
- package/dist/components/HeaderBar.js +13 -8
- package/dist/components/HeaderBar.js.map +2 -2
- package/dist/components/HistorySearchOverlay.js +4 -3
- package/dist/components/HistorySearchOverlay.js.map +2 -2
- package/dist/components/HotkeyHelpPanel.js +8 -11
- package/dist/components/HotkeyHelpPanel.js.map +2 -2
- package/dist/components/InvalidConfigDialog.js +2 -1
- package/dist/components/InvalidConfigDialog.js.map +2 -2
- package/dist/components/Logo.js +23 -67
- package/dist/components/Logo.js.map +2 -2
- package/dist/components/MCPServerApprovalDialog.js +2 -1
- package/dist/components/MCPServerApprovalDialog.js.map +2 -2
- package/dist/components/MCPServerDialogCopy.js +2 -1
- package/dist/components/MCPServerDialogCopy.js.map +2 -2
- package/dist/components/MCPServerMultiselectDialog.js +2 -1
- package/dist/components/MCPServerMultiselectDialog.js.map +2 -2
- package/dist/components/MessageSelector.js +4 -3
- package/dist/components/MessageSelector.js.map +2 -2
- package/dist/components/ModeIndicator.js +2 -1
- package/dist/components/ModeIndicator.js.map +2 -2
- package/dist/components/ModelConfig.js +4 -3
- package/dist/components/ModelConfig.js.map +2 -2
- package/dist/components/ModelListManager.js +4 -3
- package/dist/components/ModelListManager.js.map +2 -2
- package/dist/components/ModelSelector/ModelSelector.js +26 -13
- package/dist/components/ModelSelector/ModelSelector.js.map +2 -2
- package/dist/components/Onboarding.js +3 -2
- package/dist/components/Onboarding.js.map +2 -2
- package/dist/components/OperationSummary.js +130 -0
- package/dist/components/OperationSummary.js.map +7 -0
- package/dist/components/PromptInput.js +88 -75
- package/dist/components/PromptInput.js.map +2 -2
- package/dist/components/SensitiveFileWarning.js +31 -0
- package/dist/components/SensitiveFileWarning.js.map +7 -0
- package/dist/components/Spinner.js +71 -22
- package/dist/components/Spinner.js.map +2 -2
- package/dist/components/StructuredDiff.js +6 -8
- package/dist/components/StructuredDiff.js.map +2 -2
- package/dist/components/SubagentBlock.js +4 -2
- package/dist/components/SubagentBlock.js.map +2 -2
- package/dist/components/SubagentProgress.js +7 -4
- package/dist/components/SubagentProgress.js.map +2 -2
- package/dist/components/TaskCard.js +14 -11
- package/dist/components/TaskCard.js.map +2 -2
- package/dist/components/TextInput.js +9 -1
- package/dist/components/TextInput.js.map +2 -2
- package/dist/components/TodoPanel.js +44 -26
- package/dist/components/TodoPanel.js.map +2 -2
- package/dist/components/ToolUseLoader.js +2 -2
- package/dist/components/ToolUseLoader.js.map +2 -2
- package/dist/components/TreeConnector.js +4 -3
- package/dist/components/TreeConnector.js.map +2 -2
- package/dist/components/TrustDialog.js +2 -1
- package/dist/components/TrustDialog.js.map +2 -2
- package/dist/components/binary-feedback/BinaryFeedbackView.js +2 -1
- package/dist/components/binary-feedback/BinaryFeedbackView.js.map +2 -2
- package/dist/components/messages/AssistantTextMessage.js +17 -9
- package/dist/components/messages/AssistantTextMessage.js.map +2 -2
- package/dist/components/messages/AssistantToolUseMessage.js +8 -4
- package/dist/components/messages/AssistantToolUseMessage.js.map +2 -2
- package/dist/components/messages/GroupRenderer.js +2 -1
- package/dist/components/messages/GroupRenderer.js.map +2 -2
- package/dist/components/messages/NestedTasksPreview.js +13 -1
- package/dist/components/messages/NestedTasksPreview.js.map +2 -2
- package/dist/components/messages/ParallelTasksGroupView.js +4 -3
- package/dist/components/messages/ParallelTasksGroupView.js.map +2 -2
- package/dist/components/messages/TaskInModuleView.js +35 -15
- package/dist/components/messages/TaskInModuleView.js.map +2 -2
- package/dist/components/messages/TaskOutputContent.js +9 -6
- package/dist/components/messages/TaskOutputContent.js.map +2 -2
- package/dist/components/messages/UserPromptMessage.js +2 -2
- package/dist/components/messages/UserPromptMessage.js.map +2 -2
- package/dist/constants/colors.js +90 -72
- package/dist/constants/colors.js.map +2 -2
- package/dist/constants/toolInputExamples.js +84 -0
- package/dist/constants/toolInputExamples.js.map +7 -0
- package/dist/core/backupManager.js +321 -0
- package/dist/core/backupManager.js.map +7 -0
- package/dist/core/costTracker.js +9 -18
- package/dist/core/costTracker.js.map +2 -2
- package/dist/core/gitAutoCommit.js +287 -0
- package/dist/core/gitAutoCommit.js.map +7 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +2 -2
- package/dist/core/operationTracker.js +212 -0
- package/dist/core/operationTracker.js.map +7 -0
- package/dist/core/permissions/rules/allowedToolsRule.js +1 -1
- package/dist/core/permissions/rules/allowedToolsRule.js.map +2 -2
- package/dist/core/permissions/rules/autoEscalationRule.js +5 -0
- package/dist/core/permissions/rules/autoEscalationRule.js.map +2 -2
- package/dist/core/permissions/rules/projectBoundaryRule.js +5 -0
- package/dist/core/permissions/rules/projectBoundaryRule.js.map +2 -2
- package/dist/core/permissions/rules/sensitivePathsRule.js +5 -0
- package/dist/core/permissions/rules/sensitivePathsRule.js.map +2 -2
- package/dist/core/tokenStats.js +9 -0
- package/dist/core/tokenStats.js.map +7 -0
- package/dist/core/tokenStatsManager.js +331 -0
- package/dist/core/tokenStatsManager.js.map +7 -0
- package/dist/entrypoints/cli.js +115 -87
- package/dist/entrypoints/cli.js.map +2 -2
- package/dist/hooks/useAgentTokenStats.js +72 -0
- package/dist/hooks/useAgentTokenStats.js.map +7 -0
- package/dist/hooks/useAgentTranscripts.js +30 -6
- package/dist/hooks/useAgentTranscripts.js.map +2 -2
- package/dist/hooks/useLogMessages.js +12 -1
- package/dist/hooks/useLogMessages.js.map +2 -2
- package/dist/i18n/locales/en.js +6 -5
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/zh-CN.js +6 -5
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/i18n/types.js.map +1 -1
- package/dist/permissions.js +28 -1
- package/dist/permissions.js.map +2 -2
- package/dist/query.js +78 -4
- package/dist/query.js.map +3 -3
- package/dist/screens/REPL.js +23 -3
- package/dist/screens/REPL.js.map +2 -2
- package/dist/services/claude.js +54 -3
- package/dist/services/claude.js.map +2 -2
- package/dist/services/intelligentCompactor.js +1 -1
- package/dist/services/intelligentCompactor.js.map +2 -2
- package/dist/services/mcpClient.js +81 -25
- package/dist/services/mcpClient.js.map +2 -2
- package/dist/services/sandbox/filesystemBoundary.js +58 -17
- package/dist/services/sandbox/filesystemBoundary.js.map +2 -2
- package/dist/tools/AskExpertModelTool/AskExpertModelTool.js +3 -2
- package/dist/tools/AskExpertModelTool/AskExpertModelTool.js.map +2 -2
- package/dist/tools/AskUserQuestionTool/AskUserQuestionTool.js +2 -1
- package/dist/tools/AskUserQuestionTool/AskUserQuestionTool.js.map +2 -2
- package/dist/tools/BashTool/BashTool.js +22 -3
- package/dist/tools/BashTool/BashTool.js.map +2 -2
- package/dist/tools/BashTool/prompt.js +178 -34
- package/dist/tools/BashTool/prompt.js.map +2 -2
- package/dist/tools/FileEditTool/prompt.js +6 -3
- package/dist/tools/FileEditTool/prompt.js.map +2 -2
- package/dist/tools/FileWriteTool/prompt.js +4 -2
- package/dist/tools/FileWriteTool/prompt.js.map +2 -2
- package/dist/tools/MultiEditTool/prompt.js +5 -3
- package/dist/tools/MultiEditTool/prompt.js.map +2 -2
- package/dist/tools/NotebookEditTool/NotebookEditTool.js +2 -1
- package/dist/tools/NotebookEditTool/NotebookEditTool.js.map +2 -2
- package/dist/tools/PlanModeTool/EnterPlanModeTool.js +3 -2
- package/dist/tools/PlanModeTool/EnterPlanModeTool.js.map +2 -2
- package/dist/tools/PlanModeTool/ExitPlanModeTool.js +3 -2
- package/dist/tools/PlanModeTool/ExitPlanModeTool.js.map +2 -2
- package/dist/tools/PlanModeTool/prompt.js +1 -1
- package/dist/tools/PlanModeTool/prompt.js.map +1 -1
- package/dist/tools/SkillTool/SkillTool.js +4 -3
- package/dist/tools/SkillTool/SkillTool.js.map +2 -2
- package/dist/tools/SkillTool/prompt.js +1 -1
- package/dist/tools/SkillTool/prompt.js.map +1 -1
- package/dist/tools/TaskOutputTool/TaskOutputTool.js +3 -2
- package/dist/tools/TaskOutputTool/TaskOutputTool.js.map +2 -2
- package/dist/tools/TaskTool/TaskTool.js +8 -0
- package/dist/tools/TaskTool/TaskTool.js.map +2 -2
- package/dist/utils/CircuitBreaker.js +242 -0
- package/dist/utils/CircuitBreaker.js.map +7 -0
- package/dist/utils/ask.js +2 -0
- package/dist/utils/ask.js.map +2 -2
- package/dist/utils/config.js +47 -5
- package/dist/utils/config.js.map +2 -2
- package/dist/utils/credentials/CredentialStore.js +1 -0
- package/dist/utils/credentials/CredentialStore.js.map +7 -0
- package/dist/utils/credentials/EncryptedFileStore.js +157 -0
- package/dist/utils/credentials/EncryptedFileStore.js.map +7 -0
- package/dist/utils/credentials/index.js +37 -0
- package/dist/utils/credentials/index.js.map +7 -0
- package/dist/utils/credentials/migration.js +82 -0
- package/dist/utils/credentials/migration.js.map +7 -0
- package/dist/utils/markdown.js +13 -1
- package/dist/utils/markdown.js.map +2 -2
- package/dist/utils/permissions/filesystem.js +5 -1
- package/dist/utils/permissions/filesystem.js.map +2 -2
- package/dist/utils/safePath.js +132 -0
- package/dist/utils/safePath.js.map +7 -0
- package/dist/utils/sensitiveFiles.js +125 -0
- package/dist/utils/sensitiveFiles.js.map +7 -0
- package/dist/utils/taskDisplayUtils.js +9 -9
- package/dist/utils/taskDisplayUtils.js.map +2 -2
- package/dist/utils/theme.js +6 -6
- package/dist/utils/theme.js.map +1 -1
- package/dist/utils/toolRiskClassification.js +207 -0
- package/dist/utils/toolRiskClassification.js.map +7 -0
- package/dist/utils/tooling/safeRender.js +5 -4
- package/dist/utils/tooling/safeRender.js.map +2 -2
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +9 -7
- package/dist/hooks/useCancelRequest.js +0 -31
- package/dist/hooks/useCancelRequest.js.map +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/markdown.ts"],
|
|
4
|
-
"sourcesContent": ["import { marked, Token } from 'marked'\nimport { stripSystemMessages } from './messages'\nimport chalk from 'chalk'\nimport { EOL } from 'os'\nimport { highlight, supportsLanguage } from 'cli-highlight'\nimport { logError } from './log'\n\nexport function applyMarkdown(content: string): string {\n return marked\n .lexer(
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAqB;AAC9B,SAAS,2BAA2B;AACpC,OAAO,WAAW;AAClB,SAAS,WAAW;AACpB,SAAS,WAAW,wBAAwB;AAC5C,SAAS,gBAAgB;
|
|
4
|
+
"sourcesContent": ["import { marked, Token } from 'marked'\nimport { stripSystemMessages } from './messages'\nimport chalk from 'chalk'\nimport { EOL } from 'os'\nimport { highlight, supportsLanguage } from 'cli-highlight'\nimport { logError } from './log'\n\n/**\n * Strip outer markdown code block wrapper if present.\n * AI models sometimes wrap their response in ```markdown ... ``` which\n * causes the content to be parsed as a code block instead of actual markdown.\n */\nfunction stripMarkdownCodeBlockWrapper(content: string): string {\n const trimmed = content.trim()\n // Match ```markdown or ```md at the start and ``` at the end\n const codeBlockRegex = /^```(?:markdown|md)?\\s*\\n([\\s\\S]*?)\\n```$/\n const match = trimmed.match(codeBlockRegex)\n if (match) {\n return match[1] ?? trimmed\n }\n return content\n}\n\nexport function applyMarkdown(content: string): string {\n // Pre-process: remove outer markdown code block wrapper if present\n const preprocessed = stripMarkdownCodeBlockWrapper(\n stripSystemMessages(content),\n )\n return marked\n .lexer(preprocessed)\n .map(_ => format(_))\n .join('')\n .trim()\n}\n\nfunction format(\n token: Token,\n listDepth = 0,\n orderedListNumber: number | null = null,\n parent: Token | null = null,\n): string {\n switch (token.type) {\n case 'blockquote':\n return chalk.dim.italic((token.tokens ?? []).map(_ => format(_)).join(''))\n case 'code':\n if (token.lang && supportsLanguage(token.lang)) {\n return highlight(token.text, { language: token.lang }) + EOL\n } else {\n logError(\n `Language not supported while highlighting code, falling back to markdown: ${token.lang}`,\n )\n return highlight(token.text, { language: 'markdown' }) + EOL\n }\n case 'codespan':\n // inline code\n return chalk.blue(token.text)\n case 'em':\n return chalk.italic((token.tokens ?? []).map(_ => format(_)).join(''))\n case 'strong':\n return chalk.bold((token.tokens ?? []).map(_ => format(_)).join(''))\n case 'heading':\n switch (token.depth) {\n case 1: // h1\n return (\n chalk.bold.italic.underline(\n (token.tokens ?? []).map(_ => format(_)).join(''),\n ) +\n EOL +\n EOL\n )\n case 2: // h2\n return (\n chalk.bold((token.tokens ?? []).map(_ => format(_)).join('')) +\n EOL +\n EOL\n )\n default: // h3+\n return (\n chalk.bold.dim((token.tokens ?? []).map(_ => format(_)).join('')) +\n EOL +\n EOL\n )\n }\n case 'hr':\n return '---'\n case 'image':\n return `[Image: ${token.title}: ${token.href}]`\n case 'link':\n return chalk.blue(token.href)\n case 'list': {\n return token.items\n .map((_: Token, index: number) =>\n format(\n _,\n listDepth,\n token.ordered ? token.start + index : null,\n token,\n ),\n )\n .join('')\n }\n case 'list_item':\n return (token.tokens ?? [])\n .map(\n _ =>\n `${' '.repeat(listDepth)}${format(_, listDepth + 1, orderedListNumber, token)}`,\n )\n .join('')\n case 'paragraph':\n return (token.tokens ?? []).map(_ => format(_)).join('') + EOL\n case 'space':\n return EOL\n case 'text':\n if (parent?.type === 'list_item') {\n return `${orderedListNumber === null ? '-' : getListNumber(listDepth, orderedListNumber) + '.'} ${token.tokens ? token.tokens.map(_ => format(_, listDepth, orderedListNumber, token)).join('') : token.text}${EOL}`\n } else {\n return token.text\n }\n }\n // TODO: tables\n return ''\n}\n\nconst DEPTH_1_LIST_NUMBERS = [\n 'a',\n 'b',\n 'c',\n 'd',\n 'e',\n 'f',\n 'g',\n 'h',\n 'i',\n 'j',\n 'k',\n 'l',\n 'm',\n 'n',\n 'o',\n 'p',\n 'q',\n 'r',\n 's',\n 't',\n 'u',\n 'v',\n 'w',\n 'x',\n 'y',\n 'z',\n 'aa',\n 'ab',\n 'ac',\n 'ad',\n 'ae',\n 'af',\n 'ag',\n 'ah',\n 'ai',\n 'aj',\n 'ak',\n 'al',\n 'am',\n 'an',\n 'ao',\n 'ap',\n 'aq',\n 'ar',\n 'as',\n 'at',\n 'au',\n 'av',\n 'aw',\n 'ax',\n 'ay',\n 'az',\n]\nconst DEPTH_2_LIST_NUMBERS = [\n 'i',\n 'ii',\n 'iii',\n 'iv',\n 'v',\n 'vi',\n 'vii',\n 'viii',\n 'ix',\n 'x',\n 'xi',\n 'xii',\n 'xiii',\n 'xiv',\n 'xv',\n 'xvi',\n 'xvii',\n 'xviii',\n 'xix',\n 'xx',\n 'xxi',\n 'xxii',\n 'xxiii',\n 'xxiv',\n 'xxv',\n 'xxvi',\n 'xxvii',\n 'xxviii',\n 'xxix',\n 'xxx',\n 'xxxi',\n 'xxxii',\n 'xxxiii',\n 'xxxiv',\n 'xxxv',\n 'xxxvi',\n 'xxxvii',\n 'xxxviii',\n 'xxxix',\n 'xl',\n]\n\nfunction getListNumber(listDepth: number, orderedListNumber: number): string {\n switch (listDepth) {\n case 0:\n case 1:\n return orderedListNumber.toString()\n case 2:\n return DEPTH_1_LIST_NUMBERS[orderedListNumber - 1]! // TODO: don't hard code the list\n case 3:\n return DEPTH_2_LIST_NUMBERS[orderedListNumber - 1]! // TODO: don't hard code the list\n default:\n return orderedListNumber.toString()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,13 +4,17 @@ import {
|
|
|
4
4
|
getCurrentProjectConfig,
|
|
5
5
|
saveCurrentProjectConfig
|
|
6
6
|
} from "../config.js";
|
|
7
|
+
import { safeResolvePath } from "../safePath.js";
|
|
7
8
|
const readFileAllowedDirectories = /* @__PURE__ */ new Set();
|
|
8
9
|
const writeFileAllowedDirectories = /* @__PURE__ */ new Set();
|
|
9
10
|
let persistentReadPermissions = [];
|
|
10
11
|
let persistentWritePermissions = [];
|
|
11
12
|
let persistentPermissionsLoaded = false;
|
|
12
13
|
function toAbsolutePath(path) {
|
|
13
|
-
const
|
|
14
|
+
const cwd = getCwd();
|
|
15
|
+
const originalCwd = getOriginalCwd();
|
|
16
|
+
const validationResult = safeResolvePath(path, cwd, originalCwd);
|
|
17
|
+
const abs = validationResult.resolvedPath;
|
|
14
18
|
return normalizeForCompare(abs);
|
|
15
19
|
}
|
|
16
20
|
function normalizeForCompare(p) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/utils/permissions/filesystem.ts"],
|
|
4
|
-
"sourcesContent": ["import { isAbsolute, resolve, relative, dirname } from 'path'\nimport { getCwd, getOriginalCwd } from '@utils/state'\nimport {\n getCurrentProjectConfig,\n saveCurrentProjectConfig,\n} from '@utils/config'\n\n/**\n * Permission persistence mode\n */\nexport type PermissionPersistenceMode = 'session' | 'project' | 'global'\n\n/**\n * File permission entry for persistent storage\n */\nexport interface FilePermissionEntry {\n /** Path that was granted permission */\n path: string\n /** Type of permission */\n type: 'read' | 'write'\n /** When the permission was granted */\n grantedAt: number\n /** Expiration timestamp (optional) */\n expiresAt?: number\n /** Reason for granting (for audit purposes) */\n reason?: string\n}\n\n// In-memory storage for file permissions that resets each session\n// Sets of allowed directories for read and write operations\nconst readFileAllowedDirectories: Set<string> = new Set()\nconst writeFileAllowedDirectories: Set<string> = new Set()\n\n// Persistent permission entries (loaded from config)\nlet persistentReadPermissions: FilePermissionEntry[] = []\nlet persistentWritePermissions: FilePermissionEntry[] = []\nlet persistentPermissionsLoaded = false\n\n/**\n * Ensures a path is absolute by resolving it relative to cwd if necessary\n * @param path The path to normalize\n * @returns Absolute path\n */\nexport function toAbsolutePath(path: string): string {\n const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)\n return normalizeForCompare(abs)\n}\n\nfunction normalizeForCompare(p: string): string {\n // Normalize separators and resolve .. and . segments\n const norm = resolve(p)\n // On Windows, comparisons should be case-insensitive\n return process.platform === 'win32' ? norm.toLowerCase() : norm\n}\n\nfunction isSubpath(base: string, target: string): boolean {\n const rel = relative(base, target)\n // If different drive letters on Windows, relative returns the target path\n if (!rel || rel === '') return true\n // Not a subpath if it goes up to parent\n if (rel.startsWith('..')) return false\n // Not a subpath if absolute\n if (isAbsolute(rel)) return false\n return true\n}\n\n/**\n * Ensures a path is in the original cwd path\n * @param directory The directory path to normalize\n * @returns Absolute path\n */\nexport function pathInOriginalCwd(path: string): boolean {\n const absolutePath = toAbsolutePath(path)\n const base = toAbsolutePath(getOriginalCwd())\n return isSubpath(base, absolutePath)\n}\n\n/**\n * Load persistent permissions from project config\n */\nfunction loadPersistentPermissions(): void {\n if (persistentPermissionsLoaded) return\n\n try {\n const config = getCurrentProjectConfig()\n const filePermissions = (config as any).filePermissions as\n | { read?: FilePermissionEntry[]; write?: FilePermissionEntry[] }\n | undefined\n\n if (filePermissions) {\n const now = Date.now()\n\n // Load read permissions (filter out expired)\n persistentReadPermissions = (filePermissions.read || []).filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n\n // Load write permissions (filter out expired)\n persistentWritePermissions = (filePermissions.write || []).filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n }\n\n persistentPermissionsLoaded = true\n } catch {\n // Silently fail - use session-only permissions\n persistentPermissionsLoaded = true\n }\n}\n\n/**\n * Save persistent permissions to project config\n */\nfunction savePersistentPermissions(): void {\n try {\n const config = getCurrentProjectConfig()\n\n // Only save if there are permissions to save\n if (\n persistentReadPermissions.length > 0 ||\n persistentWritePermissions.length > 0\n ) {\n ;(config as any).filePermissions = {\n read: persistentReadPermissions,\n write: persistentWritePermissions,\n }\n saveCurrentProjectConfig(config)\n }\n } catch {\n // Silently fail - permissions will work for session only\n }\n}\n\n/**\n * Check if read permission exists for the specified directory\n * @param directory The directory to check permission for\n * @returns true if read permission exists, false otherwise\n */\nexport function hasReadPermission(directory: string): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(directory)\n\n // Check session permissions\n for (const allowedPath of readFileAllowedDirectories) {\n if (isSubpath(allowedPath, absolutePath)) return true\n }\n\n // Check persistent permissions\n const now = Date.now()\n for (const entry of persistentReadPermissions) {\n if (entry.expiresAt && entry.expiresAt < now) continue\n const normalizedEntry = toAbsolutePath(entry.path)\n if (isSubpath(normalizedEntry, absolutePath)) return true\n }\n\n return false\n}\n\n/**\n * Check if write permission exists for the specified directory\n * @param directory The directory to check permission for\n * @returns true if write permission exists, false otherwise\n */\nexport function hasWritePermission(directory: string): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(directory)\n\n // Check session permissions\n for (const allowedPath of writeFileAllowedDirectories) {\n if (isSubpath(allowedPath, absolutePath)) return true\n }\n\n // Check persistent permissions\n const now = Date.now()\n for (const entry of persistentWritePermissions) {\n if (entry.expiresAt && entry.expiresAt < now) continue\n const normalizedEntry = toAbsolutePath(entry.path)\n if (isSubpath(normalizedEntry, absolutePath)) return true\n }\n\n return false\n}\n\n/**\n * Save read permission for a directory\n * @param directory The directory to grant read permission for\n * @param persist Whether to persist the permission (default: session only)\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function saveReadPermission(\n directory: string,\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n const absolutePath = toAbsolutePath(directory)\n\n // Remove any existing subpaths contained by this new path\n for (const allowedPath of Array.from(readFileAllowedDirectories)) {\n if (isSubpath(absolutePath, allowedPath)) {\n readFileAllowedDirectories.delete(allowedPath)\n }\n }\n readFileAllowedDirectories.add(absolutePath)\n\n // Handle persistence\n if (persist) {\n loadPersistentPermissions()\n\n const entry: FilePermissionEntry = {\n path: absolutePath,\n type: 'read',\n grantedAt: Date.now(),\n expiresAt: expiresIn ? Date.now() + expiresIn : undefined,\n reason,\n }\n\n // Remove existing entry for same path\n persistentReadPermissions = persistentReadPermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n persistentReadPermissions.push(entry)\n savePersistentPermissions()\n }\n}\n\nexport const saveReadPermissionForTest = saveReadPermission\n\n/**\n * Grants read permission for the original project directory.\n * This is useful for initializing read access to the project root.\n */\nexport function grantReadPermissionForOriginalDir(): void {\n const originalProjectDir = getOriginalCwd()\n saveReadPermission(originalProjectDir, false)\n}\n\n/**\n * Save write permission for a directory\n * @param directory The directory to grant write permission for\n * @param persist Whether to persist the permission (default: session only)\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function saveWritePermission(\n directory: string,\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n const absolutePath = toAbsolutePath(directory)\n\n for (const allowedPath of Array.from(writeFileAllowedDirectories)) {\n if (isSubpath(absolutePath, allowedPath)) {\n writeFileAllowedDirectories.delete(allowedPath)\n }\n }\n writeFileAllowedDirectories.add(absolutePath)\n\n // Handle persistence\n if (persist) {\n loadPersistentPermissions()\n\n const entry: FilePermissionEntry = {\n path: absolutePath,\n type: 'write',\n grantedAt: Date.now(),\n expiresAt: expiresIn ? Date.now() + expiresIn : undefined,\n reason,\n }\n\n // Remove existing entry for same path\n persistentWritePermissions = persistentWritePermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n persistentWritePermissions.push(entry)\n savePersistentPermissions()\n }\n}\n\n/**\n * Grants write permission for the original project directory.\n * This is useful for initializing write access to the project root.\n */\nexport function grantWritePermissionForOriginalDir(): void {\n const originalProjectDir = getOriginalCwd()\n saveWritePermission(originalProjectDir, false)\n}\n\n/**\n * Grant permission for a specific file path\n * @param filePath The file path to grant permission for\n * @param type Permission type (read or write)\n * @param persist Whether to persist the permission\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function grantFilePermission(\n filePath: string,\n type: 'read' | 'write',\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n // Grant permission to the file's directory\n const dir = dirname(toAbsolutePath(filePath))\n\n if (type === 'read') {\n saveReadPermission(dir, persist, expiresIn, reason)\n } else {\n saveWritePermission(dir, persist, expiresIn, reason)\n }\n}\n\n/**\n * Revoke a specific persistent permission\n * @param path The path to revoke permission for\n * @param type Permission type (read or write)\n */\nexport function revokePersistentPermission(\n path: string,\n type: 'read' | 'write',\n): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(path)\n let found = false\n\n if (type === 'read') {\n const before = persistentReadPermissions.length\n persistentReadPermissions = persistentReadPermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n found = persistentReadPermissions.length < before\n } else {\n const before = persistentWritePermissions.length\n persistentWritePermissions = persistentWritePermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n found = persistentWritePermissions.length < before\n }\n\n if (found) {\n savePersistentPermissions()\n }\n\n return found\n}\n\n/**\n * Get all persistent permissions\n */\nexport function getPersistentPermissions(): {\n read: FilePermissionEntry[]\n write: FilePermissionEntry[]\n} {\n loadPersistentPermissions()\n return {\n read: [...persistentReadPermissions],\n write: [...persistentWritePermissions],\n }\n}\n\n/**\n * Clear all expired persistent permissions\n */\nexport function clearExpiredPermissions(): number {\n loadPersistentPermissions()\n\n const now = Date.now()\n const beforeRead = persistentReadPermissions.length\n const beforeWrite = persistentWritePermissions.length\n\n persistentReadPermissions = persistentReadPermissions.filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n persistentWritePermissions = persistentWritePermissions.filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n\n const cleared =\n beforeRead -\n persistentReadPermissions.length +\n (beforeWrite - persistentWritePermissions.length)\n\n if (cleared > 0) {\n savePersistentPermissions()\n }\n\n return cleared\n}\n\n// For testing purposes\nexport function clearFilePermissions(): void {\n readFileAllowedDirectories.clear()\n writeFileAllowedDirectories.clear()\n persistentReadPermissions = []\n persistentWritePermissions = []\n persistentPermissionsLoaded = false\n}\n\n/**\n * Reload persistent permissions (useful after config changes)\n */\nexport function reloadPersistentPermissions(): void {\n persistentPermissionsLoaded = false\n loadPersistentPermissions()\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,SAAS,UAAU,eAAe;AACvD,SAAS,QAAQ,sBAAsB;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import { isAbsolute, resolve, relative, dirname } from 'path'\nimport { getCwd, getOriginalCwd } from '@utils/state'\nimport {\n getCurrentProjectConfig,\n saveCurrentProjectConfig,\n} from '@utils/config'\nimport { safeResolvePath } from '../safePath'\n\n/**\n * Permission persistence mode\n */\nexport type PermissionPersistenceMode = 'session' | 'project' | 'global'\n\n/**\n * File permission entry for persistent storage\n */\nexport interface FilePermissionEntry {\n /** Path that was granted permission */\n path: string\n /** Type of permission */\n type: 'read' | 'write'\n /** When the permission was granted */\n grantedAt: number\n /** Expiration timestamp (optional) */\n expiresAt?: number\n /** Reason for granting (for audit purposes) */\n reason?: string\n}\n\n// In-memory storage for file permissions that resets each session\n// Sets of allowed directories for read and write operations\nconst readFileAllowedDirectories: Set<string> = new Set()\nconst writeFileAllowedDirectories: Set<string> = new Set()\n\n// Persistent permission entries (loaded from config)\nlet persistentReadPermissions: FilePermissionEntry[] = []\nlet persistentWritePermissions: FilePermissionEntry[] = []\nlet persistentPermissionsLoaded = false\n\n/**\n * Ensures a path is absolute by resolving it relative to cwd if necessary.\n * Uses safe resolution to handle symlinks and prevent traversal attacks.\n * @param path The path to normalize\n * @returns Absolute path\n */\nexport function toAbsolutePath(path: string): string {\n const cwd = getCwd()\n const originalCwd = getOriginalCwd()\n\n // Use safe resolution to follow symlinks and detect boundary issues\n // Allow paths to go up to the original working directory boundary\n const validationResult = safeResolvePath(path, cwd, originalCwd)\n\n // Use the safely resolved path\n const abs = validationResult.resolvedPath\n return normalizeForCompare(abs)\n}\n\nfunction normalizeForCompare(p: string): string {\n // Normalize separators and resolve .. and . segments\n const norm = resolve(p)\n // On Windows, comparisons should be case-insensitive\n return process.platform === 'win32' ? norm.toLowerCase() : norm\n}\n\nfunction isSubpath(base: string, target: string): boolean {\n const rel = relative(base, target)\n // If different drive letters on Windows, relative returns the target path\n if (!rel || rel === '') return true\n // Not a subpath if it goes up to parent\n if (rel.startsWith('..')) return false\n // Not a subpath if absolute\n if (isAbsolute(rel)) return false\n return true\n}\n\n/**\n * Ensures a path is in the original cwd path\n * @param directory The directory path to normalize\n * @returns Absolute path\n */\nexport function pathInOriginalCwd(path: string): boolean {\n const absolutePath = toAbsolutePath(path)\n const base = toAbsolutePath(getOriginalCwd())\n return isSubpath(base, absolutePath)\n}\n\n/**\n * Load persistent permissions from project config\n */\nfunction loadPersistentPermissions(): void {\n if (persistentPermissionsLoaded) return\n\n try {\n const config = getCurrentProjectConfig()\n const filePermissions = (config as any).filePermissions as\n | { read?: FilePermissionEntry[]; write?: FilePermissionEntry[] }\n | undefined\n\n if (filePermissions) {\n const now = Date.now()\n\n // Load read permissions (filter out expired)\n persistentReadPermissions = (filePermissions.read || []).filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n\n // Load write permissions (filter out expired)\n persistentWritePermissions = (filePermissions.write || []).filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n }\n\n persistentPermissionsLoaded = true\n } catch {\n // Silently fail - use session-only permissions\n persistentPermissionsLoaded = true\n }\n}\n\n/**\n * Save persistent permissions to project config\n */\nfunction savePersistentPermissions(): void {\n try {\n const config = getCurrentProjectConfig()\n\n // Only save if there are permissions to save\n if (\n persistentReadPermissions.length > 0 ||\n persistentWritePermissions.length > 0\n ) {\n ;(config as any).filePermissions = {\n read: persistentReadPermissions,\n write: persistentWritePermissions,\n }\n saveCurrentProjectConfig(config)\n }\n } catch {\n // Silently fail - permissions will work for session only\n }\n}\n\n/**\n * Check if read permission exists for the specified directory\n * @param directory The directory to check permission for\n * @returns true if read permission exists, false otherwise\n */\nexport function hasReadPermission(directory: string): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(directory)\n\n // Check session permissions\n for (const allowedPath of readFileAllowedDirectories) {\n if (isSubpath(allowedPath, absolutePath)) return true\n }\n\n // Check persistent permissions\n const now = Date.now()\n for (const entry of persistentReadPermissions) {\n if (entry.expiresAt && entry.expiresAt < now) continue\n const normalizedEntry = toAbsolutePath(entry.path)\n if (isSubpath(normalizedEntry, absolutePath)) return true\n }\n\n return false\n}\n\n/**\n * Check if write permission exists for the specified directory\n * @param directory The directory to check permission for\n * @returns true if write permission exists, false otherwise\n */\nexport function hasWritePermission(directory: string): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(directory)\n\n // Check session permissions\n for (const allowedPath of writeFileAllowedDirectories) {\n if (isSubpath(allowedPath, absolutePath)) return true\n }\n\n // Check persistent permissions\n const now = Date.now()\n for (const entry of persistentWritePermissions) {\n if (entry.expiresAt && entry.expiresAt < now) continue\n const normalizedEntry = toAbsolutePath(entry.path)\n if (isSubpath(normalizedEntry, absolutePath)) return true\n }\n\n return false\n}\n\n/**\n * Save read permission for a directory\n * @param directory The directory to grant read permission for\n * @param persist Whether to persist the permission (default: session only)\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function saveReadPermission(\n directory: string,\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n const absolutePath = toAbsolutePath(directory)\n\n // Remove any existing subpaths contained by this new path\n for (const allowedPath of Array.from(readFileAllowedDirectories)) {\n if (isSubpath(absolutePath, allowedPath)) {\n readFileAllowedDirectories.delete(allowedPath)\n }\n }\n readFileAllowedDirectories.add(absolutePath)\n\n // Handle persistence\n if (persist) {\n loadPersistentPermissions()\n\n const entry: FilePermissionEntry = {\n path: absolutePath,\n type: 'read',\n grantedAt: Date.now(),\n expiresAt: expiresIn ? Date.now() + expiresIn : undefined,\n reason,\n }\n\n // Remove existing entry for same path\n persistentReadPermissions = persistentReadPermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n persistentReadPermissions.push(entry)\n savePersistentPermissions()\n }\n}\n\nexport const saveReadPermissionForTest = saveReadPermission\n\n/**\n * Grants read permission for the original project directory.\n * This is useful for initializing read access to the project root.\n */\nexport function grantReadPermissionForOriginalDir(): void {\n const originalProjectDir = getOriginalCwd()\n saveReadPermission(originalProjectDir, false)\n}\n\n/**\n * Save write permission for a directory\n * @param directory The directory to grant write permission for\n * @param persist Whether to persist the permission (default: session only)\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function saveWritePermission(\n directory: string,\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n const absolutePath = toAbsolutePath(directory)\n\n for (const allowedPath of Array.from(writeFileAllowedDirectories)) {\n if (isSubpath(absolutePath, allowedPath)) {\n writeFileAllowedDirectories.delete(allowedPath)\n }\n }\n writeFileAllowedDirectories.add(absolutePath)\n\n // Handle persistence\n if (persist) {\n loadPersistentPermissions()\n\n const entry: FilePermissionEntry = {\n path: absolutePath,\n type: 'write',\n grantedAt: Date.now(),\n expiresAt: expiresIn ? Date.now() + expiresIn : undefined,\n reason,\n }\n\n // Remove existing entry for same path\n persistentWritePermissions = persistentWritePermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n persistentWritePermissions.push(entry)\n savePersistentPermissions()\n }\n}\n\n/**\n * Grants write permission for the original project directory.\n * This is useful for initializing write access to the project root.\n */\nexport function grantWritePermissionForOriginalDir(): void {\n const originalProjectDir = getOriginalCwd()\n saveWritePermission(originalProjectDir, false)\n}\n\n/**\n * Grant permission for a specific file path\n * @param filePath The file path to grant permission for\n * @param type Permission type (read or write)\n * @param persist Whether to persist the permission\n * @param expiresIn Optional expiration time in milliseconds\n * @param reason Optional reason for granting permission\n */\nexport function grantFilePermission(\n filePath: string,\n type: 'read' | 'write',\n persist: boolean = false,\n expiresIn?: number,\n reason?: string,\n): void {\n // Grant permission to the file's directory\n const dir = dirname(toAbsolutePath(filePath))\n\n if (type === 'read') {\n saveReadPermission(dir, persist, expiresIn, reason)\n } else {\n saveWritePermission(dir, persist, expiresIn, reason)\n }\n}\n\n/**\n * Revoke a specific persistent permission\n * @param path The path to revoke permission for\n * @param type Permission type (read or write)\n */\nexport function revokePersistentPermission(\n path: string,\n type: 'read' | 'write',\n): boolean {\n loadPersistentPermissions()\n\n const absolutePath = toAbsolutePath(path)\n let found = false\n\n if (type === 'read') {\n const before = persistentReadPermissions.length\n persistentReadPermissions = persistentReadPermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n found = persistentReadPermissions.length < before\n } else {\n const before = persistentWritePermissions.length\n persistentWritePermissions = persistentWritePermissions.filter(\n p => toAbsolutePath(p.path) !== absolutePath,\n )\n found = persistentWritePermissions.length < before\n }\n\n if (found) {\n savePersistentPermissions()\n }\n\n return found\n}\n\n/**\n * Get all persistent permissions\n */\nexport function getPersistentPermissions(): {\n read: FilePermissionEntry[]\n write: FilePermissionEntry[]\n} {\n loadPersistentPermissions()\n return {\n read: [...persistentReadPermissions],\n write: [...persistentWritePermissions],\n }\n}\n\n/**\n * Clear all expired persistent permissions\n */\nexport function clearExpiredPermissions(): number {\n loadPersistentPermissions()\n\n const now = Date.now()\n const beforeRead = persistentReadPermissions.length\n const beforeWrite = persistentWritePermissions.length\n\n persistentReadPermissions = persistentReadPermissions.filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n persistentWritePermissions = persistentWritePermissions.filter(\n p => !p.expiresAt || p.expiresAt > now,\n )\n\n const cleared =\n beforeRead -\n persistentReadPermissions.length +\n (beforeWrite - persistentWritePermissions.length)\n\n if (cleared > 0) {\n savePersistentPermissions()\n }\n\n return cleared\n}\n\n// For testing purposes\nexport function clearFilePermissions(): void {\n readFileAllowedDirectories.clear()\n writeFileAllowedDirectories.clear()\n persistentReadPermissions = []\n persistentWritePermissions = []\n persistentPermissionsLoaded = false\n}\n\n/**\n * Reload persistent permissions (useful after config changes)\n */\nexport function reloadPersistentPermissions(): void {\n persistentPermissionsLoaded = false\n loadPersistentPermissions()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { realpathSync, existsSync, lstatSync } from "fs";
|
|
2
|
+
import { resolve, isAbsolute, relative, sep, join } from "path";
|
|
3
|
+
function safeResolvePath(inputPath, cwd, allowedRoot) {
|
|
4
|
+
try {
|
|
5
|
+
const absolutePath = isAbsolute(inputPath) ? resolve(inputPath) : resolve(cwd, inputPath);
|
|
6
|
+
let isSymlink = false;
|
|
7
|
+
let realPath = absolutePath;
|
|
8
|
+
if (existsSync(absolutePath)) {
|
|
9
|
+
try {
|
|
10
|
+
isSymlink = lstatSync(absolutePath).isSymbolicLink();
|
|
11
|
+
realPath = realpathSync(absolutePath);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
realPath = absolutePath;
|
|
14
|
+
}
|
|
15
|
+
} else {
|
|
16
|
+
realPath = findRealPathForNonexistent(absolutePath);
|
|
17
|
+
}
|
|
18
|
+
return validateBoundary(realPath, allowedRoot, isSymlink);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
resolvedPath: inputPath,
|
|
23
|
+
isSymlink: false,
|
|
24
|
+
error: `Failed to validate path: ${e instanceof Error ? e.message : String(e)}`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function findRealPathForNonexistent(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
let currentPath = filePath;
|
|
31
|
+
let depth = 0;
|
|
32
|
+
const maxDepth = 50;
|
|
33
|
+
while (!existsSync(currentPath) && depth < maxDepth) {
|
|
34
|
+
const parent = resolve(currentPath, "..");
|
|
35
|
+
if (parent === currentPath) break;
|
|
36
|
+
currentPath = parent;
|
|
37
|
+
depth++;
|
|
38
|
+
}
|
|
39
|
+
if (existsSync(currentPath)) {
|
|
40
|
+
const realParent = realpathSync(currentPath);
|
|
41
|
+
let remaining = filePath.slice(currentPath.length);
|
|
42
|
+
if (remaining.startsWith(sep)) {
|
|
43
|
+
remaining = remaining.slice(1);
|
|
44
|
+
}
|
|
45
|
+
return remaining ? join(realParent, remaining) : realParent;
|
|
46
|
+
}
|
|
47
|
+
return resolve(filePath);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return resolve(filePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function validateBoundary(resolvedPath, allowedRoot, isSymlink) {
|
|
53
|
+
let normalizedRoot;
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(allowedRoot)) {
|
|
56
|
+
normalizedRoot = realpathSync(allowedRoot);
|
|
57
|
+
} else {
|
|
58
|
+
normalizedRoot = resolve(allowedRoot);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
normalizedRoot = resolve(allowedRoot);
|
|
62
|
+
}
|
|
63
|
+
normalizedRoot = normalizePath(normalizedRoot);
|
|
64
|
+
const normalizedResolved = normalizePath(resolvedPath);
|
|
65
|
+
const rel = relative(normalizedRoot, normalizedResolved);
|
|
66
|
+
if (rel.startsWith("..")) {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
resolvedPath,
|
|
70
|
+
isSymlink,
|
|
71
|
+
error: `Path escapes sandbox boundary: ${resolvedPath} is outside ${normalizedRoot}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (isAbsolute(rel)) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
resolvedPath,
|
|
78
|
+
isSymlink,
|
|
79
|
+
error: `Path is on different drive or root: ${resolvedPath}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
valid: true,
|
|
84
|
+
resolvedPath,
|
|
85
|
+
isSymlink
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function normalizePath(p) {
|
|
89
|
+
const resolved = resolve(p);
|
|
90
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
91
|
+
}
|
|
92
|
+
function hasSymlinksInPath(filePath) {
|
|
93
|
+
try {
|
|
94
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
95
|
+
const parts = absolutePath.split(sep).filter((p) => p);
|
|
96
|
+
let currentPath = "";
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (currentPath) {
|
|
99
|
+
currentPath = resolve(currentPath, part);
|
|
100
|
+
} else {
|
|
101
|
+
currentPath = sep === "\\" ? `${part}\\` : `/${part}`;
|
|
102
|
+
}
|
|
103
|
+
if (!existsSync(currentPath)) break;
|
|
104
|
+
try {
|
|
105
|
+
if (lstatSync(currentPath).isSymbolicLink()) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function getRealPath(filePath) {
|
|
117
|
+
try {
|
|
118
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
119
|
+
if (existsSync(absolutePath)) {
|
|
120
|
+
return realpathSync(absolutePath);
|
|
121
|
+
}
|
|
122
|
+
return absolutePath;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return filePath;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
getRealPath,
|
|
129
|
+
hasSymlinksInPath,
|
|
130
|
+
safeResolvePath
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=safePath.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/safePath.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Safe Path Resolution Utility\n *\n * Provides secure path resolution with symbolic link tracking and boundary validation.\n * Prevents attackers from escaping sandbox boundaries using symlinks.\n */\n\nimport { realpathSync, existsSync, lstatSync } from 'fs'\nimport { resolve, isAbsolute, relative, sep, join } from 'path'\n\n/**\n * Result of a path validation operation\n */\nexport interface PathValidationResult {\n /** Whether the path is valid and within boundaries */\n valid: boolean\n /** The resolved real path (after following symlinks) */\n resolvedPath: string\n /** Whether the path itself is a symbolic link */\n isSymlink: boolean\n /** Error message if validation failed */\n error?: string\n}\n\n/**\n * Safely resolves a path to its real location, tracking symbolic links.\n * Validates that the final resolved path stays within the allowed boundary.\n *\n * @param inputPath The path to resolve (relative or absolute)\n * @param cwd Current working directory for relative path resolution\n * @param allowedRoot The root directory boundary that must not be escaped\n * @returns PathValidationResult with validation status and resolved path\n *\n * @example\n * const result = safeResolvePath('./file.txt', '/project', '/project')\n * if (result.valid) {\n * // Path is valid and doesn't escape boundary\n * console.log(result.resolvedPath)\n * }\n */\nexport function safeResolvePath(\n inputPath: string,\n cwd: string,\n allowedRoot: string,\n): PathValidationResult {\n try {\n // 1. Resolve to absolute path\n const absolutePath = isAbsolute(inputPath)\n ? resolve(inputPath)\n : resolve(cwd, inputPath)\n\n // 2. Determine if path is a symlink and get real path\n let isSymlink = false\n let realPath = absolutePath\n\n if (existsSync(absolutePath)) {\n // Path exists - check if it's a symlink and follow to real location\n try {\n isSymlink = lstatSync(absolutePath).isSymbolicLink()\n realPath = realpathSync(absolutePath)\n } catch (e) {\n // If stat/realpath fails, use absolute path\n realPath = absolutePath\n }\n } else {\n // Path doesn't exist - try to resolve via parents to detect symlink escapes\n realPath = findRealPathForNonexistent(absolutePath)\n }\n\n // 3. Validate that realPath is within allowed boundary\n return validateBoundary(realPath, allowedRoot, isSymlink)\n } catch (e) {\n return {\n valid: false,\n resolvedPath: inputPath,\n isSymlink: false,\n error: `Failed to validate path: ${e instanceof Error ? e.message : String(e)}`,\n }\n }\n}\n\n/**\n * Attempts to resolve a non-existent path by resolving existing parents.\n * This helps detect symlink escapes for paths that don't yet exist.\n */\nfunction findRealPathForNonexistent(filePath: string): string {\n try {\n // Walk up the path to find the first existing parent\n let currentPath = filePath\n let depth = 0\n const maxDepth = 50 // Prevent infinite loops\n\n while (!existsSync(currentPath) && depth < maxDepth) {\n const parent = resolve(currentPath, '..')\n if (parent === currentPath) break // Reached filesystem root\n currentPath = parent\n depth++\n }\n\n // If we found an existing path, resolve it and reconstruct\n if (existsSync(currentPath)) {\n const realParent = realpathSync(currentPath)\n // Remove the parent path and any leading separator from the remaining portion\n let remaining = filePath.slice(currentPath.length)\n if (remaining.startsWith(sep)) {\n remaining = remaining.slice(1)\n }\n return remaining ? join(realParent, remaining) : realParent\n }\n\n // Fallback: just return resolved path\n return resolve(filePath)\n } catch (e) {\n return resolve(filePath)\n }\n}\n\n/**\n * Validates that a resolved path stays within the allowed boundary.\n *\n * @param resolvedPath The absolute path to validate\n * @param allowedRoot The boundary root directory\n * @param isSymlink Whether the original path was a symlink\n * @returns PathValidationResult indicating if boundary is respected\n */\nfunction validateBoundary(\n resolvedPath: string,\n allowedRoot: string,\n isSymlink: boolean,\n): PathValidationResult {\n let normalizedRoot: string\n\n try {\n // Resolve the boundary root to its real path\n if (existsSync(allowedRoot)) {\n normalizedRoot = realpathSync(allowedRoot)\n } else {\n normalizedRoot = resolve(allowedRoot)\n }\n } catch (e) {\n normalizedRoot = resolve(allowedRoot)\n }\n\n // Normalize both paths for comparison (handle case sensitivity)\n normalizedRoot = normalizePath(normalizedRoot)\n const normalizedResolved = normalizePath(resolvedPath)\n\n // Check if the resolved path is within the boundary\n const rel = relative(normalizedRoot, normalizedResolved)\n\n // Validation fails if:\n // 1. Path goes up to parent directory (..)\n if (rel.startsWith('..')) {\n return {\n valid: false,\n resolvedPath,\n isSymlink,\n error: `Path escapes sandbox boundary: ${resolvedPath} is outside ${normalizedRoot}`,\n }\n }\n\n // 2. Relative path is absolute (different drive on Windows)\n if (isAbsolute(rel)) {\n return {\n valid: false,\n resolvedPath,\n isSymlink,\n error: `Path is on different drive or root: ${resolvedPath}`,\n }\n }\n\n return {\n valid: true,\n resolvedPath,\n isSymlink,\n }\n}\n\n/**\n * Normalizes a path for platform-consistent comparison.\n * On Windows, converts to lowercase for case-insensitive comparison.\n *\n * @param p The path to normalize\n * @returns Normalized path string\n */\nfunction normalizePath(p: string): string {\n const resolved = resolve(p)\n return process.platform === 'win32' ? resolved.toLowerCase() : resolved\n}\n\n/**\n * Checks if a path contains any symlinks in its chain.\n * This is useful for detecting potential symlink attacks.\n *\n * @param filePath The path to check\n * @returns true if any component in the path is a symlink, false otherwise\n *\n * @example\n * const hasSymlinks = hasSymlinksInPath('/path/to/file')\n * if (hasSymlinks) {\n * console.log('Path contains one or more symlinks')\n * }\n */\nexport function hasSymlinksInPath(filePath: string): boolean {\n try {\n const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath)\n const parts = absolutePath.split(sep).filter(p => p)\n let currentPath = ''\n\n for (const part of parts) {\n if (currentPath) {\n currentPath = resolve(currentPath, part)\n } else {\n // Handle root directory\n currentPath = sep === '\\\\' ? `${part}\\\\` : `/${part}`\n }\n\n if (!existsSync(currentPath)) break\n\n try {\n if (lstatSync(currentPath).isSymbolicLink()) {\n return true\n }\n } catch (e) {\n // If we can't stat it, continue\n }\n }\n\n return false\n } catch (e) {\n return false\n }\n}\n\n/**\n * Gets the real path of a file, following symlinks.\n * Returns the original path if it can't be resolved.\n *\n * @param filePath The path to resolve\n * @returns The real path, or the input path if resolution fails\n */\nexport function getRealPath(filePath: string): string {\n try {\n const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath)\n if (existsSync(absolutePath)) {\n return realpathSync(absolutePath)\n }\n return absolutePath\n } catch (e) {\n return filePath\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const YOLO_WARNING_PATTERNS = [
|
|
2
|
+
// Environment files
|
|
3
|
+
"**/.env",
|
|
4
|
+
"**/.env.*",
|
|
5
|
+
// Credential files
|
|
6
|
+
"**/credentials.json",
|
|
7
|
+
"**/secrets.*",
|
|
8
|
+
// Private keys and certificates
|
|
9
|
+
"**/*.pem",
|
|
10
|
+
"**/*.key",
|
|
11
|
+
"**/*.p12",
|
|
12
|
+
"**/*.pfx",
|
|
13
|
+
// Package manager auth
|
|
14
|
+
"**/.npmrc",
|
|
15
|
+
"**/.pypirc",
|
|
16
|
+
// SSH keys
|
|
17
|
+
"**/id_rsa",
|
|
18
|
+
"**/id_ed25519",
|
|
19
|
+
"**/id_ecdsa",
|
|
20
|
+
"**/id_dsa",
|
|
21
|
+
// Other auth files
|
|
22
|
+
"**/.netrc",
|
|
23
|
+
"**/.pgpass",
|
|
24
|
+
"**/config/secrets.*",
|
|
25
|
+
// AWS credentials
|
|
26
|
+
"**/.aws/credentials",
|
|
27
|
+
"**/.aws/config",
|
|
28
|
+
// Google Cloud
|
|
29
|
+
"**/service-account*.json",
|
|
30
|
+
"**/gcloud/credentials.db",
|
|
31
|
+
// Docker secrets
|
|
32
|
+
"**/.docker/config.json",
|
|
33
|
+
// Kubernetes secrets
|
|
34
|
+
"**/kubeconfig",
|
|
35
|
+
"**/.kube/config"
|
|
36
|
+
];
|
|
37
|
+
function isSensitiveFile(filePath) {
|
|
38
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
39
|
+
const fileName = normalizedPath.split("/").pop() || "";
|
|
40
|
+
for (const pattern of YOLO_WARNING_PATTERNS) {
|
|
41
|
+
if (pattern.startsWith("**/")) {
|
|
42
|
+
const filePattern = pattern.slice(3);
|
|
43
|
+
if (filePattern.includes("/")) {
|
|
44
|
+
if (matchPathSuffix(normalizedPath, filePattern)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
if (matchFilePattern(fileName, filePattern)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
if (matchFilePattern(normalizedPath, pattern)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function matchPathSuffix(path, suffixPattern) {
|
|
61
|
+
const regexPattern = suffixPattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
62
|
+
const regex = new RegExp(`(^|/)${regexPattern}$`);
|
|
63
|
+
return regex.test(path);
|
|
64
|
+
}
|
|
65
|
+
function matchFilePattern(str, pattern) {
|
|
66
|
+
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "\xA7\xA7").replace(/\*/g, "[^/]*").replace(/§§/g, ".*").replace(/\?/g, ".");
|
|
67
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
68
|
+
return regex.test(str);
|
|
69
|
+
}
|
|
70
|
+
function getSensitiveFileReason(filePath) {
|
|
71
|
+
const normalizedPath = filePath.replace(/\\/g, "/").toLowerCase();
|
|
72
|
+
const fileName = normalizedPath.split("/").pop() || "";
|
|
73
|
+
if (fileName === ".env" || fileName.startsWith(".env.")) {
|
|
74
|
+
return "Environment variables file - may contain API keys and secrets";
|
|
75
|
+
}
|
|
76
|
+
if (fileName === "credentials.json") {
|
|
77
|
+
return "Credentials file - may contain authentication tokens";
|
|
78
|
+
}
|
|
79
|
+
if (fileName.startsWith("secrets.") || fileName.includes("/secrets.")) {
|
|
80
|
+
return "Secrets file - may contain sensitive configuration";
|
|
81
|
+
}
|
|
82
|
+
if (fileName.endsWith(".pem") || fileName.endsWith(".key")) {
|
|
83
|
+
return "Private key file - cryptographic key material";
|
|
84
|
+
}
|
|
85
|
+
if (fileName.endsWith(".p12") || fileName.endsWith(".pfx")) {
|
|
86
|
+
return "Certificate file - may contain private keys";
|
|
87
|
+
}
|
|
88
|
+
if (fileName === ".npmrc") {
|
|
89
|
+
return "npm configuration - may contain registry tokens";
|
|
90
|
+
}
|
|
91
|
+
if (fileName === ".pypirc") {
|
|
92
|
+
return "PyPI configuration - may contain upload tokens";
|
|
93
|
+
}
|
|
94
|
+
if (fileName === "id_rsa" || fileName === "id_ed25519" || fileName === "id_ecdsa" || fileName === "id_dsa") {
|
|
95
|
+
return "SSH private key - authentication key";
|
|
96
|
+
}
|
|
97
|
+
if (fileName === ".netrc") {
|
|
98
|
+
return "Netrc file - may contain login credentials";
|
|
99
|
+
}
|
|
100
|
+
if (fileName === ".pgpass") {
|
|
101
|
+
return "PostgreSQL password file";
|
|
102
|
+
}
|
|
103
|
+
if (normalizedPath.includes("/.aws/")) {
|
|
104
|
+
return "AWS configuration - may contain access keys";
|
|
105
|
+
}
|
|
106
|
+
if (fileName.startsWith("service-account") && fileName.endsWith(".json")) {
|
|
107
|
+
return "Service account key - cloud authentication";
|
|
108
|
+
}
|
|
109
|
+
if (normalizedPath.includes("/.docker/config.json")) {
|
|
110
|
+
return "Docker configuration - may contain registry credentials";
|
|
111
|
+
}
|
|
112
|
+
if (fileName === "kubeconfig" || normalizedPath.includes("/.kube/config")) {
|
|
113
|
+
return "Kubernetes configuration - cluster access credentials";
|
|
114
|
+
}
|
|
115
|
+
if (isSensitiveFile(filePath)) {
|
|
116
|
+
return "May contain sensitive information";
|
|
117
|
+
}
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
YOLO_WARNING_PATTERNS,
|
|
122
|
+
getSensitiveFileReason,
|
|
123
|
+
isSensitiveFile
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=sensitiveFiles.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/sensitiveFiles.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sensitive File Detection Utility\n *\n * Provides detection for files that may contain sensitive information\n * (API keys, credentials, private keys, etc.).\n *\n * Used in YOLO mode to display informational warnings without blocking operations.\n */\n\n/**\n * Patterns for files that should trigger warnings in YOLO mode.\n * These files commonly contain sensitive information like:\n * - API keys and tokens\n * - Database credentials\n * - Private keys and certificates\n * - Authentication secrets\n */\nexport const YOLO_WARNING_PATTERNS = [\n // Environment files\n '**/.env',\n '**/.env.*',\n\n // Credential files\n '**/credentials.json',\n '**/secrets.*',\n\n // Private keys and certificates\n '**/*.pem',\n '**/*.key',\n '**/*.p12',\n '**/*.pfx',\n\n // Package manager auth\n '**/.npmrc',\n '**/.pypirc',\n\n // SSH keys\n '**/id_rsa',\n '**/id_ed25519',\n '**/id_ecdsa',\n '**/id_dsa',\n\n // Other auth files\n '**/.netrc',\n '**/.pgpass',\n '**/config/secrets.*',\n\n // AWS credentials\n '**/.aws/credentials',\n '**/.aws/config',\n\n // Google Cloud\n '**/service-account*.json',\n '**/gcloud/credentials.db',\n\n // Docker secrets\n '**/.docker/config.json',\n\n // Kubernetes secrets\n '**/kubeconfig',\n '**/.kube/config',\n] as const\n\n/**\n * Check if a file path matches sensitive file patterns.\n * Returns true if the file should trigger a warning (but not block) in YOLO mode.\n *\n * @param filePath - The file path to check (absolute or relative)\n * @returns true if the file matches a sensitive file pattern\n *\n * @example\n * ```typescript\n * isSensitiveFile('/path/to/.env') // true\n * isSensitiveFile('/path/to/config.ts') // false\n * isSensitiveFile('credentials.json') // true\n * isSensitiveFile('/home/user/.aws/credentials') // true\n * ```\n */\nexport function isSensitiveFile(filePath: string): boolean {\n // Normalize path separators (Windows backslashes to forward slashes)\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Extract the file name for patterns that only match file names\n const fileName = normalizedPath.split('/').pop() || ''\n\n for (const pattern of YOLO_WARNING_PATTERNS) {\n // Handle ** prefix (any directory depth)\n if (pattern.startsWith('**/')) {\n const filePattern = pattern.slice(3)\n\n // Check if the pattern itself contains path separators (like .aws/credentials)\n if (filePattern.includes('/')) {\n // Match against the full path suffix\n if (matchPathSuffix(normalizedPath, filePattern)) {\n return true\n }\n } else {\n // Match just the filename\n if (matchFilePattern(fileName, filePattern)) {\n return true\n }\n }\n } else {\n // Direct pattern match against the full path\n if (matchFilePattern(normalizedPath, pattern)) {\n return true\n }\n }\n }\n\n return false\n}\n\n/**\n * Check if a path ends with a given suffix pattern.\n * Handles glob patterns like .aws/credentials or config/secrets.*\n *\n * @param path - The full normalized path\n * @param suffixPattern - The pattern to match at the end of the path\n */\nfunction matchPathSuffix(path: string, suffixPattern: string): boolean {\n // Build regex from the suffix pattern\n const regexPattern = suffixPattern\n .replace(/\\./g, '\\\\.')\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.')\n\n // Match at the end of the path (with optional leading /)\n const regex = new RegExp(`(^|/)${regexPattern}$`)\n return regex.test(path)\n}\n\n/**\n * Match a string against a glob-like pattern.\n * Supports * (any characters) and ? (single character) wildcards.\n *\n * @param str - The string to test\n * @param pattern - The glob pattern (e.g., \"*.pem\", \".env.*\")\n */\nfunction matchFilePattern(str: string, pattern: string): boolean {\n // Convert glob pattern to regex\n const regexPattern = pattern\n .replace(/\\./g, '\\\\.')\n .replace(/\\*\\*/g, '\u00A7\u00A7') // Temporarily replace ** to avoid double processing\n .replace(/\\*/g, '[^/]*') // * matches anything except path separator\n .replace(/\u00A7\u00A7/g, '.*') // ** matches anything including path separators\n .replace(/\\?/g, '.')\n\n const regex = new RegExp(`^${regexPattern}$`)\n return regex.test(str)\n}\n\n/**\n * Get a human-readable description of why a file is considered sensitive.\n *\n * @param filePath - The file path to describe\n * @returns A description string or undefined if not sensitive\n */\nexport function getSensitiveFileReason(filePath: string): string | undefined {\n const normalizedPath = filePath.replace(/\\\\/g, '/').toLowerCase()\n const fileName = normalizedPath.split('/').pop() || ''\n\n // Check specific patterns and return appropriate descriptions\n if (fileName === '.env' || fileName.startsWith('.env.')) {\n return 'Environment variables file - may contain API keys and secrets'\n }\n\n if (fileName === 'credentials.json') {\n return 'Credentials file - may contain authentication tokens'\n }\n\n if (fileName.startsWith('secrets.') || fileName.includes('/secrets.')) {\n return 'Secrets file - may contain sensitive configuration'\n }\n\n if (fileName.endsWith('.pem') || fileName.endsWith('.key')) {\n return 'Private key file - cryptographic key material'\n }\n\n if (fileName.endsWith('.p12') || fileName.endsWith('.pfx')) {\n return 'Certificate file - may contain private keys'\n }\n\n if (fileName === '.npmrc') {\n return 'npm configuration - may contain registry tokens'\n }\n\n if (fileName === '.pypirc') {\n return 'PyPI configuration - may contain upload tokens'\n }\n\n if (\n fileName === 'id_rsa' ||\n fileName === 'id_ed25519' ||\n fileName === 'id_ecdsa' ||\n fileName === 'id_dsa'\n ) {\n return 'SSH private key - authentication key'\n }\n\n if (fileName === '.netrc') {\n return 'Netrc file - may contain login credentials'\n }\n\n if (fileName === '.pgpass') {\n return 'PostgreSQL password file'\n }\n\n if (normalizedPath.includes('/.aws/')) {\n return 'AWS configuration - may contain access keys'\n }\n\n if (fileName.startsWith('service-account') && fileName.endsWith('.json')) {\n return 'Service account key - cloud authentication'\n }\n\n if (normalizedPath.includes('/.docker/config.json')) {\n return 'Docker configuration - may contain registry credentials'\n }\n\n if (fileName === 'kubeconfig' || normalizedPath.includes('/.kube/config')) {\n return 'Kubernetes configuration - cluster access credentials'\n }\n\n // Generic fallback for files that match patterns but don't have specific descriptions\n if (isSensitiveFile(filePath)) {\n return 'May contain sensitive information'\n }\n\n return undefined\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAgentTranscript, getAgentIdByToolUseId } from "./agentTranscripts.js";
|
|
2
|
-
import {
|
|
2
|
+
import { SYMBOL_COLORS } from "../constants/colors.js";
|
|
3
3
|
import { SYMBOLS } from "../constants/symbols.js";
|
|
4
4
|
function getLatestOutput(transcript) {
|
|
5
5
|
const messages = transcript.messages;
|
|
@@ -74,8 +74,8 @@ function getStatusIcon(status) {
|
|
|
74
74
|
return SYMBOLS.TOOL_RUNNING;
|
|
75
75
|
// ◐ 半圆 - in progress
|
|
76
76
|
case "completed":
|
|
77
|
-
return
|
|
78
|
-
//
|
|
77
|
+
return SYMBOLS.TOOL_SUCCESS;
|
|
78
|
+
// ✓ 勾 - completed
|
|
79
79
|
case "failed":
|
|
80
80
|
return SYMBOLS.TOOL_ERROR;
|
|
81
81
|
// ✗ - failed
|
|
@@ -88,17 +88,17 @@ function getStatusIcon(status) {
|
|
|
88
88
|
function getStatusColor(status) {
|
|
89
89
|
switch (status) {
|
|
90
90
|
case "pending":
|
|
91
|
-
return
|
|
91
|
+
return SYMBOL_COLORS.pending;
|
|
92
92
|
case "running":
|
|
93
|
-
return
|
|
93
|
+
return SYMBOL_COLORS.running;
|
|
94
94
|
case "completed":
|
|
95
|
-
return
|
|
95
|
+
return SYMBOL_COLORS.success;
|
|
96
96
|
case "failed":
|
|
97
|
-
return
|
|
97
|
+
return SYMBOL_COLORS.error;
|
|
98
98
|
case "interrupted":
|
|
99
|
-
return
|
|
99
|
+
return SYMBOL_COLORS.pending;
|
|
100
100
|
default:
|
|
101
|
-
return
|
|
101
|
+
return SYMBOL_COLORS.pending;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
function formatDuration(durationMs) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utils/taskDisplayUtils.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Task Display Utilities\n *\n * Utilities for computing task display content with truncation rules.\n * Implements V3's intermediate state display logic.\n */\n\nimport type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'\nimport type { AgentTranscript } from './agentTranscripts'\nimport type {\n DisplayConfig,\n TaskDisplayContent,\n} from '@minto-types/messageGroup'\nimport { getAgentTranscript, getAgentIdByToolUseId } from './agentTranscripts'\nimport { SEMANTIC_COLORS } from '@constants/colors'\nimport { SYMBOLS } from '@constants/symbols'\n\n/**\n * Get the latest text output from a transcript\n */\nexport function getLatestOutput(transcript: AgentTranscript): string {\n const messages = transcript.messages\n\n // Search from the end for the most recent text output\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i]\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n const textBlock = content.find(b => b.type === 'text')\n if (textBlock && 'text' in textBlock) {\n return textBlock.text\n }\n }\n }\n\n return ''\n}\n\n/**\n * Get all nested Task transcripts from a parent transcript\n */\nexport function getNestedTasks(transcript: AgentTranscript): AgentTranscript[] {\n const nested: AgentTranscript[] = []\n\n for (const msg of transcript.messages) {\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_use' && block.name === 'Task') {\n const toolUse = block as ToolUseBlockParam\n const nestedAgentId = getAgentIdByToolUseId(toolUse.id)\n if (nestedAgentId) {\n const nestedTranscript = getAgentTranscript(nestedAgentId)\n if (nestedTranscript) {\n nested.push(nestedTranscript)\n }\n }\n }\n }\n }\n }\n\n return nested\n}\n\n/**\n * Truncate text to specified length with ellipsis\n */\nexport function truncate(text: string, maxLength: number): string {\n if (!text) return ''\n // Clean up whitespace\n const cleaned = text.trim().replace(/\\n+/g, ' ').replace(/\\s+/g, ' ')\n if (cleaned.length <= maxLength) return cleaned\n return cleaned.slice(0, maxLength - 3) + '...'\n}\n\n/**\n * Get task display content with truncation rules (from V3)\n */\nexport function getTaskDisplayContent(\n transcript: AgentTranscript,\n config: DisplayConfig,\n): TaskDisplayContent {\n const nestedTasks = getNestedTasks(transcript)\n\n if (nestedTasks.length === 0) {\n // No children: show latest output, truncated\n const latestOutput = getLatestOutput(transcript)\n return {\n type: 'simple',\n content: truncate(latestOutput, config.maxCharsWithoutChildren),\n }\n }\n\n // Has children: show recent children based on config\n const tasksToShow = config.showAllChildren\n ? nestedTasks\n : nestedTasks.slice(-config.maxRecentChildren)\n\n const children = tasksToShow.map(task => ({\n description: task.description,\n status: task.status,\n content: truncate(getLatestOutput(task), config.maxCharsPerChild),\n }))\n\n return {\n type: 'nested',\n children,\n hiddenCount: nestedTasks.length - tasksToShow.length,\n }\n}\n\n/**\n * Get status icon for a task status\n */\nexport function getStatusIcon(status: string): string {\n switch (status) {\n case 'pending':\n return '\u25CB' // \u7A7A\u5FC3\u5706 - pending\n case 'running':\n return SYMBOLS.TOOL_RUNNING // \u25D0 \u534A\u5706 - in progress\n case 'completed':\n return '\u25CF' // \u5B9E\u5FC3\u5706 - completed (\u89C6\u89C9\u8FDB\u5EA6: \u25CB\u2192\u25D0\u2192\u25CF)\n case 'failed':\n return SYMBOLS.TOOL_ERROR // \u2717 - failed\n case 'interrupted':\n return '\u2298'\n default:\n return '\u25CB'\n }\n}\n\n/**\n * Get status color for a task status\n */\nexport function getStatusColor(status: string): string {\n switch (status) {\n case 'pending':\n return SEMANTIC_COLORS.dim\n case 'running':\n return SEMANTIC_COLORS.running\n case 'completed':\n return SEMANTIC_COLORS.success\n case 'failed':\n return SEMANTIC_COLORS.error\n case 'interrupted':\n return SEMANTIC_COLORS.dim\n default:\n return SEMANTIC_COLORS.dim\n }\n}\n\n// ============================================================================\n// \u5143\u4FE1\u606F\u683C\u5F0F\u5316 (Meta Info Formatting)\n// \u57FA\u4E8E REPL \u663E\u793A\u89C4\u8303\uFF1A\u8017\u65F6 \u00B7 \u6A21\u578B \u00B7 \u65F6\u95F4\n// ============================================================================\n\n/**\n * \u5143\u4FE1\u606F\u6570\u636E\u7ED3\u6784\n */\nexport interface MetaInfo {\n /** \u8017\u65F6\uFF08\u6BEB\u79D2\uFF09 */\n duration?: number\n /** \u6A21\u578B\u540D\u79F0 */\n model?: string\n /** \u65F6\u95F4\u6233 */\n timestamp?: Date | number\n}\n\n/**\n * \u683C\u5F0F\u5316\u8017\u65F6\n * - < 60s: X.Xs (\u5982 2.4s)\n * - >= 60s: Xm Xs (\u5982 1m 23s)\n * - \u672A\u77E5: --\n */\nexport function formatDuration(durationMs?: number): string {\n if (durationMs === undefined || durationMs === null) {\n return '--'\n }\n\n const seconds = durationMs / 1000\n\n if (seconds < 60) {\n // \u5C0F\u4E8E 60 \u79D2\uFF0C\u663E\u793A X.Xs\n return `${seconds.toFixed(1)}s`\n }\n\n // \u5927\u4E8E\u7B49\u4E8E 60 \u79D2\uFF0C\u663E\u793A Xm Xs\n const minutes = Math.floor(seconds / 60)\n const remainingSeconds = Math.floor(seconds % 60)\n return `${minutes}m ${remainingSeconds}s`\n}\n\n/**\n * \u683C\u5F0F\u5316\u6A21\u578B\u540D\u79F0\uFF08\u7B80\u5199\uFF09\n * - claude-3-opus-20240229 -> opus\n * - claude-3-sonnet-20240229 -> sonnet\n * - glm-4.7 -> glm-4.7\n */\nexport function formatModelName(model?: string): string {\n if (!model) return ''\n\n // Claude \u6A21\u578B\u7B80\u5199\n if (model.includes('opus')) return 'opus'\n if (model.includes('sonnet')) return 'sonnet'\n if (model.includes('haiku')) return 'haiku'\n\n // \u5176\u4ED6\u6A21\u578B\u4FDD\u6301\u539F\u6837\u6216\u622A\u65AD\n // \u5982\u679C\u540D\u79F0\u592A\u957F\uFF0C\u53D6\u6700\u540E\u4E00\u90E8\u5206\n const parts = model.split('-')\n if (parts.length > 2 && model.length > 15) {\n // \u5C1D\u8BD5\u63D0\u53D6\u6709\u610F\u4E49\u7684\u90E8\u5206\n return parts.slice(0, 2).join('-')\n }\n\n return model\n}\n\n/**\n * \u683C\u5F0F\u5316\u65F6\u95F4\u6233\n * \u683C\u5F0F: H:MM (24\u5C0F\u65F6\u5236)\n */\nexport function formatTimestamp(timestamp?: Date | number): string {\n if (!timestamp) return ''\n\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp)\n const hours = date.getHours()\n const minutes = date.getMinutes().toString().padStart(2, '0')\n\n return `${hours}:${minutes}`\n}\n\n/**\n * \u683C\u5F0F\u5316\u5143\u4FE1\u606F\n *\n * \u89C4\u5219\uFF1A\n * - \u6709\u8017\u65F6\uFF1A\u8017\u65F6 \u00B7 \u6A21\u578B \u00B7 \u65F6\u95F4 (\u5982 \"2.4s \u00B7 glm-4.7 \u00B7 9:21\")\n * - \u65E0\u8017\u65F6\uFF1A\u6A21\u578B \u00B7 \u65F6\u95F4 (\u5982 \"glm-4.7 \u00B7 9:21\")\n *\n * @param meta - \u5143\u4FE1\u606F\u5BF9\u8C61\n * @returns \u683C\u5F0F\u5316\u540E\u7684\u5B57\u7B26\u4E32\uFF0C\u5982\u679C\u6CA1\u6709\u4EFB\u4F55\u4FE1\u606F\u5219\u8FD4\u56DE\u7A7A\u5B57\u7B26\u4E32\n */\nexport function formatMetaInfo(meta: MetaInfo): string {\n const parts: string[] = []\n\n // \u6709\u8017\u65F6\u65F6\uFF0C\u8017\u65F6\u653E\u5728\u6700\u524D\u9762\n if (meta.duration !== undefined && meta.duration !== null) {\n parts.push(formatDuration(meta.duration))\n }\n\n // \u6A21\u578B\u540D\u79F0\n const modelName = formatModelName(meta.model)\n if (modelName) {\n parts.push(modelName)\n }\n\n // \u65F6\u95F4\u6233\n const time = formatTimestamp(meta.timestamp)\n if (time) {\n parts.push(time)\n }\n\n // \u4F7F\u7528 \" \u00B7 \" \u5206\u9694\n return parts.join(' \u00B7 ')\n}\n\n/**\n * Tool use history item\n */\nexport interface ToolUseHistoryItem {\n /** Tool name */\n name: string\n /** Tool use ID */\n id: string\n /** Brief description of what the tool did */\n description: string\n /** Timestamp (message index for ordering) */\n messageIndex: number\n /** Whether this tool is currently executing (no result yet) */\n isExecuting?: boolean\n}\n\n/**\n * Get tool use history from a transcript\n * Returns tool uses in chronological order (oldest first)\n * Also marks tools that are currently executing (no result yet)\n */\nexport function getToolUseHistory(\n transcript: AgentTranscript,\n): ToolUseHistoryItem[] {\n const history: ToolUseHistoryItem[] = []\n\n // Collect all tool_result IDs to identify completed tools\n const completedToolIds = new Set<string>()\n for (const msg of transcript.messages) {\n if (msg.type === 'user') {\n const content = msg.message.content\n if (!Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_result' && 'tool_use_id' in block) {\n completedToolIds.add(block.tool_use_id)\n }\n }\n }\n }\n\n for (let i = 0; i < transcript.messages.length; i++) {\n const msg = transcript.messages[i]\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_use') {\n const toolUse = block as ToolUseBlockParam\n const description = getToolUseDescription(toolUse)\n const isExecuting = !completedToolIds.has(toolUse.id)\n\n history.push({\n name: toolUse.name,\n id: toolUse.id,\n description,\n messageIndex: i,\n isExecuting,\n })\n }\n }\n }\n }\n\n return history\n}\n\n/**\n * Extract just the filename from a path for concise display\n */\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] || filePath\n}\n\n/**\n * Map internal tool names to user-friendly display names\n * Note: Some tools have different internal names (e.g., 'View' \u2192 'Read')\n */\nconst TOOL_DISPLAY_NAMES: Record<string, string> = {\n View: 'Read',\n // Add more mappings as needed\n}\n\n/**\n * Get the user-friendly display name for a tool\n */\nexport function getToolDisplayName(internalName: string): string {\n return TOOL_DISPLAY_NAMES[internalName] || internalName\n}\n\n/**\n * Get a brief description of what a tool use did\n * Note: Description should NOT include the tool name - it's displayed separately\n */\nfunction getToolUseDescription(toolUse: ToolUseBlockParam): string {\n const input = toolUse.input as Record<string, unknown>\n\n switch (toolUse.name) {\n case 'Read':\n case 'View': // View is aliased to Read\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Write':\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Edit':\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Glob':\n return input.pattern ? String(input.pattern) : 'files'\n case 'Grep':\n return input.pattern ? `\"${input.pattern}\"` : 'content'\n case 'Bash':\n return input.command\n ? String(input.command).slice(0, 50) +\n (String(input.command).length > 50 ? '...' : '')\n : 'command'\n case 'Task':\n return input.description ? String(input.description) : 'sub-task'\n case 'WebFetch':\n return input.url ? String(input.url) : 'URL'\n case 'WebSearch':\n return input.query ? String(input.query) : 'web'\n default:\n // Try to extract a meaningful description from common input fields\n // Note: Don't include tool name in description - it's displayed separately\n if (input.file_path) return getFileName(String(input.file_path))\n if (input.path) return getFileName(String(input.path))\n if (input.query) return String(input.query)\n if (input.pattern) return String(input.pattern)\n return ''\n }\n}\n\n/**\n * Get tool use history for display, respecting display config\n * @param transcript The agent transcript\n * @param config Display configuration\n * @returns Tool uses to display (recent first for normal mode, chronological for verbose)\n */\nexport function getToolUseHistoryForDisplay(\n transcript: AgentTranscript,\n config: DisplayConfig,\n): { tools: ToolUseHistoryItem[]; hiddenCount: number } {\n const allTools = getToolUseHistory(transcript)\n\n if (config.showAllChildren) {\n // Verbose mode: show all tools in chronological order\n return { tools: allTools, hiddenCount: 0 }\n }\n\n // Normal mode: show recent N tools (most recent last for display)\n const maxTools = config.maxRecentChildren\n if (allTools.length <= maxTools) {\n return { tools: allTools, hiddenCount: 0 }\n }\n\n // Take the most recent tools\n const recentTools = allTools.slice(-maxTools)\n return {\n tools: recentTools,\n hiddenCount: allTools.length - maxTools,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAaA,SAAS,oBAAoB,6BAA6B;AAC1D,
|
|
4
|
+
"sourcesContent": ["/**\n * Task Display Utilities\n *\n * Utilities for computing task display content with truncation rules.\n * Implements V3's intermediate state display logic.\n */\n\nimport type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'\nimport type { AgentTranscript } from './agentTranscripts'\nimport type {\n DisplayConfig,\n TaskDisplayContent,\n} from '@minto-types/messageGroup'\nimport { getAgentTranscript, getAgentIdByToolUseId } from './agentTranscripts'\nimport { SEMANTIC_COLORS, SYMBOL_COLORS } from '@constants/colors'\nimport { SYMBOLS } from '@constants/symbols'\n\n/**\n * Get the latest text output from a transcript\n */\nexport function getLatestOutput(transcript: AgentTranscript): string {\n const messages = transcript.messages\n\n // Search from the end for the most recent text output\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i]\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n const textBlock = content.find(b => b.type === 'text')\n if (textBlock && 'text' in textBlock) {\n return textBlock.text\n }\n }\n }\n\n return ''\n}\n\n/**\n * Get all nested Task transcripts from a parent transcript\n */\nexport function getNestedTasks(transcript: AgentTranscript): AgentTranscript[] {\n const nested: AgentTranscript[] = []\n\n for (const msg of transcript.messages) {\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_use' && block.name === 'Task') {\n const toolUse = block as ToolUseBlockParam\n const nestedAgentId = getAgentIdByToolUseId(toolUse.id)\n if (nestedAgentId) {\n const nestedTranscript = getAgentTranscript(nestedAgentId)\n if (nestedTranscript) {\n nested.push(nestedTranscript)\n }\n }\n }\n }\n }\n }\n\n return nested\n}\n\n/**\n * Truncate text to specified length with ellipsis\n */\nexport function truncate(text: string, maxLength: number): string {\n if (!text) return ''\n // Clean up whitespace\n const cleaned = text.trim().replace(/\\n+/g, ' ').replace(/\\s+/g, ' ')\n if (cleaned.length <= maxLength) return cleaned\n return cleaned.slice(0, maxLength - 3) + '...'\n}\n\n/**\n * Get task display content with truncation rules (from V3)\n */\nexport function getTaskDisplayContent(\n transcript: AgentTranscript,\n config: DisplayConfig,\n): TaskDisplayContent {\n const nestedTasks = getNestedTasks(transcript)\n\n if (nestedTasks.length === 0) {\n // No children: show latest output, truncated\n const latestOutput = getLatestOutput(transcript)\n return {\n type: 'simple',\n content: truncate(latestOutput, config.maxCharsWithoutChildren),\n }\n }\n\n // Has children: show recent children based on config\n const tasksToShow = config.showAllChildren\n ? nestedTasks\n : nestedTasks.slice(-config.maxRecentChildren)\n\n const children = tasksToShow.map(task => ({\n description: task.description,\n status: task.status,\n content: truncate(getLatestOutput(task), config.maxCharsPerChild),\n }))\n\n return {\n type: 'nested',\n children,\n hiddenCount: nestedTasks.length - tasksToShow.length,\n }\n}\n\n/**\n * Get status icon for a task status\n * \u9075\u5FAA REPL \u89C4\u8303\uFF1A\n * - pending: \u25CB (\u7A7A\u5FC3\u5706)\n * - running: \u25D0 (\u534A\u5706\uFF0CTOOL_RUNNING)\n * - completed: \u2713 (\u52FE\uFF0CTOOL_SUCCESS)\n * - failed: \u2717 (\u53C9\uFF0CTOOL_ERROR)\n */\nexport function getStatusIcon(status: string): string {\n switch (status) {\n case 'pending':\n return '\u25CB' // \u7A7A\u5FC3\u5706 - pending\n case 'running':\n return SYMBOLS.TOOL_RUNNING // \u25D0 \u534A\u5706 - in progress\n case 'completed':\n return SYMBOLS.TOOL_SUCCESS // \u2713 \u52FE - completed\n case 'failed':\n return SYMBOLS.TOOL_ERROR // \u2717 - failed\n case 'interrupted':\n return '\u2298'\n default:\n return '\u25CB'\n }\n}\n\n/**\n * Get status color for a task status\n * \u4F7F\u7528 SYMBOL_COLORS \u4E0E Logo \u54C1\u724C\u914D\u8272\u4FDD\u6301\u4E00\u81F4\n */\nexport function getStatusColor(status: string): string {\n switch (status) {\n case 'pending':\n return SYMBOL_COLORS.pending\n case 'running':\n return SYMBOL_COLORS.running\n case 'completed':\n return SYMBOL_COLORS.success\n case 'failed':\n return SYMBOL_COLORS.error\n case 'interrupted':\n return SYMBOL_COLORS.pending\n default:\n return SYMBOL_COLORS.pending\n }\n}\n\n// ============================================================================\n// \u5143\u4FE1\u606F\u683C\u5F0F\u5316 (Meta Info Formatting)\n// \u57FA\u4E8E REPL \u663E\u793A\u89C4\u8303\uFF1A\u8017\u65F6 \u00B7 \u6A21\u578B \u00B7 \u65F6\u95F4\n// ============================================================================\n\n/**\n * \u5143\u4FE1\u606F\u6570\u636E\u7ED3\u6784\n */\nexport interface MetaInfo {\n /** \u8017\u65F6\uFF08\u6BEB\u79D2\uFF09 */\n duration?: number\n /** \u6A21\u578B\u540D\u79F0 */\n model?: string\n /** \u65F6\u95F4\u6233 */\n timestamp?: Date | number\n}\n\n/**\n * \u683C\u5F0F\u5316\u8017\u65F6\n * - < 60s: X.Xs (\u5982 2.4s)\n * - >= 60s: Xm Xs (\u5982 1m 23s)\n * - \u672A\u77E5: --\n */\nexport function formatDuration(durationMs?: number): string {\n if (durationMs === undefined || durationMs === null) {\n return '--'\n }\n\n const seconds = durationMs / 1000\n\n if (seconds < 60) {\n // \u5C0F\u4E8E 60 \u79D2\uFF0C\u663E\u793A X.Xs\n return `${seconds.toFixed(1)}s`\n }\n\n // \u5927\u4E8E\u7B49\u4E8E 60 \u79D2\uFF0C\u663E\u793A Xm Xs\n const minutes = Math.floor(seconds / 60)\n const remainingSeconds = Math.floor(seconds % 60)\n return `${minutes}m ${remainingSeconds}s`\n}\n\n/**\n * \u683C\u5F0F\u5316\u6A21\u578B\u540D\u79F0\uFF08\u7B80\u5199\uFF09\n * - claude-3-opus-20240229 -> opus\n * - claude-3-sonnet-20240229 -> sonnet\n * - glm-4.7 -> glm-4.7\n */\nexport function formatModelName(model?: string): string {\n if (!model) return ''\n\n // Claude \u6A21\u578B\u7B80\u5199\n if (model.includes('opus')) return 'opus'\n if (model.includes('sonnet')) return 'sonnet'\n if (model.includes('haiku')) return 'haiku'\n\n // \u5176\u4ED6\u6A21\u578B\u4FDD\u6301\u539F\u6837\u6216\u622A\u65AD\n // \u5982\u679C\u540D\u79F0\u592A\u957F\uFF0C\u53D6\u6700\u540E\u4E00\u90E8\u5206\n const parts = model.split('-')\n if (parts.length > 2 && model.length > 15) {\n // \u5C1D\u8BD5\u63D0\u53D6\u6709\u610F\u4E49\u7684\u90E8\u5206\n return parts.slice(0, 2).join('-')\n }\n\n return model\n}\n\n/**\n * \u683C\u5F0F\u5316\u65F6\u95F4\u6233\n * \u683C\u5F0F: H:MM (24\u5C0F\u65F6\u5236)\n */\nexport function formatTimestamp(timestamp?: Date | number): string {\n if (!timestamp) return ''\n\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp)\n const hours = date.getHours()\n const minutes = date.getMinutes().toString().padStart(2, '0')\n\n return `${hours}:${minutes}`\n}\n\n/**\n * \u683C\u5F0F\u5316\u5143\u4FE1\u606F\n *\n * \u89C4\u5219\uFF1A\n * - \u6709\u8017\u65F6\uFF1A\u8017\u65F6 \u00B7 \u6A21\u578B \u00B7 \u65F6\u95F4 (\u5982 \"2.4s \u00B7 glm-4.7 \u00B7 9:21\")\n * - \u65E0\u8017\u65F6\uFF1A\u6A21\u578B \u00B7 \u65F6\u95F4 (\u5982 \"glm-4.7 \u00B7 9:21\")\n *\n * @param meta - \u5143\u4FE1\u606F\u5BF9\u8C61\n * @returns \u683C\u5F0F\u5316\u540E\u7684\u5B57\u7B26\u4E32\uFF0C\u5982\u679C\u6CA1\u6709\u4EFB\u4F55\u4FE1\u606F\u5219\u8FD4\u56DE\u7A7A\u5B57\u7B26\u4E32\n */\nexport function formatMetaInfo(meta: MetaInfo): string {\n const parts: string[] = []\n\n // \u6709\u8017\u65F6\u65F6\uFF0C\u8017\u65F6\u653E\u5728\u6700\u524D\u9762\n if (meta.duration !== undefined && meta.duration !== null) {\n parts.push(formatDuration(meta.duration))\n }\n\n // \u6A21\u578B\u540D\u79F0\n const modelName = formatModelName(meta.model)\n if (modelName) {\n parts.push(modelName)\n }\n\n // \u65F6\u95F4\u6233\n const time = formatTimestamp(meta.timestamp)\n if (time) {\n parts.push(time)\n }\n\n // \u4F7F\u7528 \" \u00B7 \" \u5206\u9694\n return parts.join(' \u00B7 ')\n}\n\n/**\n * Tool use history item\n */\nexport interface ToolUseHistoryItem {\n /** Tool name */\n name: string\n /** Tool use ID */\n id: string\n /** Brief description of what the tool did */\n description: string\n /** Timestamp (message index for ordering) */\n messageIndex: number\n /** Whether this tool is currently executing (no result yet) */\n isExecuting?: boolean\n}\n\n/**\n * Get tool use history from a transcript\n * Returns tool uses in chronological order (oldest first)\n * Also marks tools that are currently executing (no result yet)\n */\nexport function getToolUseHistory(\n transcript: AgentTranscript,\n): ToolUseHistoryItem[] {\n const history: ToolUseHistoryItem[] = []\n\n // Collect all tool_result IDs to identify completed tools\n const completedToolIds = new Set<string>()\n for (const msg of transcript.messages) {\n if (msg.type === 'user') {\n const content = msg.message.content\n if (!Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_result' && 'tool_use_id' in block) {\n completedToolIds.add(block.tool_use_id)\n }\n }\n }\n }\n\n for (let i = 0; i < transcript.messages.length; i++) {\n const msg = transcript.messages[i]\n if (msg.type === 'assistant') {\n const content = msg.message.content\n if (!content || !Array.isArray(content)) continue\n\n for (const block of content) {\n if (block.type === 'tool_use') {\n const toolUse = block as ToolUseBlockParam\n const description = getToolUseDescription(toolUse)\n const isExecuting = !completedToolIds.has(toolUse.id)\n\n history.push({\n name: toolUse.name,\n id: toolUse.id,\n description,\n messageIndex: i,\n isExecuting,\n })\n }\n }\n }\n }\n\n return history\n}\n\n/**\n * Extract just the filename from a path for concise display\n */\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] || filePath\n}\n\n/**\n * Map internal tool names to user-friendly display names\n * Note: Some tools have different internal names (e.g., 'View' \u2192 'Read')\n */\nconst TOOL_DISPLAY_NAMES: Record<string, string> = {\n View: 'Read',\n // Add more mappings as needed\n}\n\n/**\n * Get the user-friendly display name for a tool\n */\nexport function getToolDisplayName(internalName: string): string {\n return TOOL_DISPLAY_NAMES[internalName] || internalName\n}\n\n/**\n * Get a brief description of what a tool use did\n * Note: Description should NOT include the tool name - it's displayed separately\n */\nfunction getToolUseDescription(toolUse: ToolUseBlockParam): string {\n const input = toolUse.input as Record<string, unknown>\n\n switch (toolUse.name) {\n case 'Read':\n case 'View': // View is aliased to Read\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Write':\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Edit':\n return input.file_path ? getFileName(String(input.file_path)) : 'file'\n case 'Glob':\n return input.pattern ? String(input.pattern) : 'files'\n case 'Grep':\n return input.pattern ? `\"${input.pattern}\"` : 'content'\n case 'Bash':\n return input.command\n ? String(input.command).slice(0, 50) +\n (String(input.command).length > 50 ? '...' : '')\n : 'command'\n case 'Task':\n return input.description ? String(input.description) : 'sub-task'\n case 'WebFetch':\n return input.url ? String(input.url) : 'URL'\n case 'WebSearch':\n return input.query ? String(input.query) : 'web'\n default:\n // Try to extract a meaningful description from common input fields\n // Note: Don't include tool name in description - it's displayed separately\n if (input.file_path) return getFileName(String(input.file_path))\n if (input.path) return getFileName(String(input.path))\n if (input.query) return String(input.query)\n if (input.pattern) return String(input.pattern)\n return ''\n }\n}\n\n/**\n * Get tool use history for display, respecting display config\n * @param transcript The agent transcript\n * @param config Display configuration\n * @returns Tool uses to display (recent first for normal mode, chronological for verbose)\n */\nexport function getToolUseHistoryForDisplay(\n transcript: AgentTranscript,\n config: DisplayConfig,\n): { tools: ToolUseHistoryItem[]; hiddenCount: number } {\n const allTools = getToolUseHistory(transcript)\n\n if (config.showAllChildren) {\n // Verbose mode: show all tools in chronological order\n return { tools: allTools, hiddenCount: 0 }\n }\n\n // Normal mode: show recent N tools (most recent last for display)\n const maxTools = config.maxRecentChildren\n if (allTools.length <= maxTools) {\n return { tools: allTools, hiddenCount: 0 }\n }\n\n // Take the most recent tools\n const recentTools = allTools.slice(-maxTools)\n return {\n tools: recentTools,\n hiddenCount: allTools.length - maxTools,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|