@google/gemini-cli-core 0.39.0-preview.2 → 0.40.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/docs/changelogs/index.md +21 -0
  2. package/dist/docs/changelogs/latest.md +253 -407
  3. package/dist/docs/changelogs/preview.md +236 -247
  4. package/dist/docs/cli/auto-memory.md +143 -0
  5. package/dist/docs/cli/enterprise.md +1 -1
  6. package/dist/docs/cli/plan-mode.md +3 -1
  7. package/dist/docs/cli/settings.md +29 -25
  8. package/dist/docs/cli/system-prompt.md +6 -6
  9. package/dist/docs/cli/telemetry.md +18 -11
  10. package/dist/docs/cli/tutorials/memory-management.md +2 -0
  11. package/dist/docs/get-started/{authentication.md → authentication.mdx} +139 -93
  12. package/dist/docs/get-started/index.md +3 -2
  13. package/dist/docs/get-started/installation.mdx +201 -0
  14. package/dist/docs/index.md +2 -2
  15. package/dist/docs/reference/configuration.md +88 -12
  16. package/dist/docs/reference/policy-engine.md +18 -12
  17. package/dist/docs/reference/tools.md +22 -0
  18. package/dist/docs/sidebar.json +13 -1
  19. package/dist/docs/tools/mcp-resources.md +44 -0
  20. package/dist/docs/tools/mcp-server.md +2 -1
  21. package/dist/docs/tools/tracker.md +61 -0
  22. package/dist/src/agents/agent-tool.js +1 -0
  23. package/dist/src/agents/agent-tool.js.map +1 -1
  24. package/dist/src/agents/agentLoader.d.ts +4 -4
  25. package/dist/src/agents/generalist-agent.js +3 -2
  26. package/dist/src/agents/generalist-agent.js.map +1 -1
  27. package/dist/src/agents/local-executor.js +4 -1
  28. package/dist/src/agents/local-executor.js.map +1 -1
  29. package/dist/src/agents/registry.js +0 -8
  30. package/dist/src/agents/registry.js.map +1 -1
  31. package/dist/src/agents/skill-extraction-agent.d.ts +3 -2
  32. package/dist/src/agents/skill-extraction-agent.js +72 -67
  33. package/dist/src/agents/skill-extraction-agent.js.map +1 -1
  34. package/dist/src/agents/skill-extraction-agent.test.js +54 -0
  35. package/dist/src/agents/skill-extraction-agent.test.js.map +1 -0
  36. package/dist/src/config/config.d.ts +21 -6
  37. package/dist/src/config/config.js +103 -19
  38. package/dist/src/config/config.js.map +1 -1
  39. package/dist/src/config/config.test.js +262 -6
  40. package/dist/src/config/config.test.js.map +1 -1
  41. package/dist/src/config/constants.d.ts +1 -0
  42. package/dist/src/config/constants.js +2 -0
  43. package/dist/src/config/constants.js.map +1 -1
  44. package/dist/src/config/memory.js +1 -1
  45. package/dist/src/config/memory.js.map +1 -1
  46. package/dist/src/config/path-validation.test.js +15 -6
  47. package/dist/src/config/path-validation.test.js.map +1 -1
  48. package/dist/src/config/projectRegistry.js +113 -32
  49. package/dist/src/config/projectRegistry.js.map +1 -1
  50. package/dist/src/config/projectRegistry.test.js +51 -0
  51. package/dist/src/config/projectRegistry.test.js.map +1 -1
  52. package/dist/src/config/storage.d.ts +3 -1
  53. package/dist/src/config/storage.js +6 -0
  54. package/dist/src/config/storage.js.map +1 -1
  55. package/dist/src/config/storage.test.js +12 -0
  56. package/dist/src/config/storage.test.js.map +1 -1
  57. package/dist/src/core/contentGenerator.d.ts +8 -1
  58. package/dist/src/core/contentGenerator.js +46 -4
  59. package/dist/src/core/contentGenerator.js.map +1 -1
  60. package/dist/src/core/contentGenerator.test.js +174 -8
  61. package/dist/src/core/contentGenerator.test.js.map +1 -1
  62. package/dist/src/core/geminiChat.test.js +1 -0
  63. package/dist/src/core/geminiChat.test.js.map +1 -1
  64. package/dist/src/core/geminiChat_network_retry.test.js +42 -0
  65. package/dist/src/core/geminiChat_network_retry.test.js.map +1 -1
  66. package/dist/src/core/localLiteRtLmClient.js +2 -0
  67. package/dist/src/core/localLiteRtLmClient.js.map +1 -1
  68. package/dist/src/core/localLiteRtLmClient.test.js +7 -0
  69. package/dist/src/core/localLiteRtLmClient.test.js.map +1 -1
  70. package/dist/src/core/loggingContentGenerator.js +3 -0
  71. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  72. package/dist/src/core/loggingContentGenerator.test.js +1 -0
  73. package/dist/src/core/loggingContentGenerator.test.js.map +1 -1
  74. package/dist/src/core/prompts.d.ts +1 -1
  75. package/dist/src/core/prompts.js +2 -2
  76. package/dist/src/core/prompts.js.map +1 -1
  77. package/dist/src/core/prompts.test.js +2 -2
  78. package/dist/src/core/prompts.test.js.map +1 -1
  79. package/dist/src/generated/git-commit.d.ts +2 -2
  80. package/dist/src/generated/git-commit.js +2 -2
  81. package/dist/src/ide/ide-client.js +3 -4
  82. package/dist/src/ide/ide-client.js.map +1 -1
  83. package/dist/src/policy/policies/read-only.toml +4 -1
  84. package/dist/src/policy/policy-engine.js +4 -0
  85. package/dist/src/policy/policy-engine.js.map +1 -1
  86. package/dist/src/policy/policy-engine.test.js +25 -0
  87. package/dist/src/policy/policy-engine.test.js.map +1 -1
  88. package/dist/src/policy/toml-loader.test.js +5 -0
  89. package/dist/src/policy/toml-loader.test.js.map +1 -1
  90. package/dist/src/prompts/promptProvider.d.ts +1 -1
  91. package/dist/src/prompts/promptProvider.js +15 -7
  92. package/dist/src/prompts/promptProvider.js.map +1 -1
  93. package/dist/src/prompts/promptProvider.test.js +31 -1
  94. package/dist/src/prompts/promptProvider.test.js.map +1 -1
  95. package/dist/src/prompts/snippets-memory-v2.test.js +94 -0
  96. package/dist/src/prompts/snippets-memory-v2.test.js.map +1 -0
  97. package/dist/src/prompts/snippets.d.ts +19 -1
  98. package/dist/src/prompts/snippets.js +24 -3
  99. package/dist/src/prompts/snippets.js.map +1 -1
  100. package/dist/src/prompts/snippets.legacy.d.ts +6 -1
  101. package/dist/src/prompts/snippets.legacy.js +10 -3
  102. package/dist/src/prompts/snippets.legacy.js.map +1 -1
  103. package/dist/src/sandbox/linux/LinuxSandboxManager.js +16 -2
  104. package/dist/src/sandbox/linux/LinuxSandboxManager.js.map +1 -1
  105. package/dist/src/sandbox/linux/bwrapArgsBuilder.js +64 -36
  106. package/dist/src/sandbox/linux/bwrapArgsBuilder.js.map +1 -1
  107. package/dist/src/sandbox/linux/bwrapArgsBuilder.test.js +3 -3
  108. package/dist/src/sandbox/linux/bwrapArgsBuilder.test.js.map +1 -1
  109. package/dist/src/sandbox/macos/MacOsSandboxManager.js +13 -1
  110. package/dist/src/sandbox/macos/MacOsSandboxManager.js.map +1 -1
  111. package/dist/src/sandbox/macos/seatbeltArgsBuilder.js +61 -42
  112. package/dist/src/sandbox/macos/seatbeltArgsBuilder.js.map +1 -1
  113. package/dist/src/sandbox/windows/WindowsSandboxManager.js +12 -1
  114. package/dist/src/sandbox/windows/WindowsSandboxManager.js.map +1 -1
  115. package/dist/src/scheduler/policy.js +1 -2
  116. package/dist/src/scheduler/policy.js.map +1 -1
  117. package/dist/src/scheduler/policy.test.js +35 -2
  118. package/dist/src/scheduler/policy.test.js.map +1 -1
  119. package/dist/src/scheduler/scheduler.js +1 -0
  120. package/dist/src/scheduler/scheduler.js.map +1 -1
  121. package/dist/src/scheduler/scheduler.test.js +2 -0
  122. package/dist/src/scheduler/scheduler.test.js.map +1 -1
  123. package/dist/src/scheduler/scheduler_hooks.test.js +1 -0
  124. package/dist/src/scheduler/scheduler_hooks.test.js.map +1 -1
  125. package/dist/src/scheduler/scheduler_parallel.test.js +1 -0
  126. package/dist/src/scheduler/scheduler_parallel.test.js.map +1 -1
  127. package/dist/src/scheduler/tool-executor.js +1 -0
  128. package/dist/src/scheduler/tool-executor.js.map +1 -1
  129. package/dist/src/services/chatRecordingService.d.ts +1 -0
  130. package/dist/src/services/chatRecordingService.js +48 -19
  131. package/dist/src/services/chatRecordingService.js.map +1 -1
  132. package/dist/src/services/memoryService.d.ts +15 -1
  133. package/dist/src/services/memoryService.js +276 -57
  134. package/dist/src/services/memoryService.js.map +1 -1
  135. package/dist/src/services/memoryService.test.js +296 -2
  136. package/dist/src/services/memoryService.test.js.map +1 -1
  137. package/dist/src/services/sandboxManager.integration.test.js +204 -0
  138. package/dist/src/services/sandboxManager.integration.test.js.map +1 -1
  139. package/dist/src/services/sandboxManager.js +16 -0
  140. package/dist/src/services/sandboxManager.js.map +1 -1
  141. package/dist/src/services/sessionSummaryUtils.d.ts +1 -1
  142. package/dist/src/services/sessionSummaryUtils.js +111 -38
  143. package/dist/src/services/sessionSummaryUtils.js.map +1 -1
  144. package/dist/src/services/sessionSummaryUtils.test.js +204 -51
  145. package/dist/src/services/sessionSummaryUtils.test.js.map +1 -1
  146. package/dist/src/services/shellExecutionService.js +7 -1
  147. package/dist/src/services/shellExecutionService.js.map +1 -1
  148. package/dist/src/telemetry/config.js +3 -0
  149. package/dist/src/telemetry/config.js.map +1 -1
  150. package/dist/src/telemetry/conseca-logger.test.js +1 -0
  151. package/dist/src/telemetry/conseca-logger.test.js.map +1 -1
  152. package/dist/src/telemetry/loggers.test.js +72 -4
  153. package/dist/src/telemetry/loggers.test.js.map +1 -1
  154. package/dist/src/telemetry/memory-monitor.d.ts +1 -0
  155. package/dist/src/telemetry/memory-monitor.js +8 -1
  156. package/dist/src/telemetry/memory-monitor.js.map +1 -1
  157. package/dist/src/telemetry/memory-monitor.test.js +6 -1
  158. package/dist/src/telemetry/memory-monitor.test.js.map +1 -1
  159. package/dist/src/telemetry/trace.d.ts +1 -0
  160. package/dist/src/telemetry/trace.js +33 -13
  161. package/dist/src/telemetry/trace.js.map +1 -1
  162. package/dist/src/telemetry/trace.test.js +50 -10
  163. package/dist/src/telemetry/trace.test.js.map +1 -1
  164. package/dist/src/telemetry/types.js +11 -4
  165. package/dist/src/telemetry/types.js.map +1 -1
  166. package/dist/src/tools/definitions/base-declarations.d.ts +2 -0
  167. package/dist/src/tools/definitions/base-declarations.js +3 -0
  168. package/dist/src/tools/definitions/base-declarations.js.map +1 -1
  169. package/dist/src/tools/definitions/coreTools.d.ts +3 -1
  170. package/dist/src/tools/definitions/coreTools.js +13 -1
  171. package/dist/src/tools/definitions/coreTools.js.map +1 -1
  172. package/dist/src/tools/definitions/model-family-sets/default-legacy.js +29 -1
  173. package/dist/src/tools/definitions/model-family-sets/default-legacy.js.map +1 -1
  174. package/dist/src/tools/definitions/model-family-sets/gemini-3.js +29 -1
  175. package/dist/src/tools/definitions/model-family-sets/gemini-3.js.map +1 -1
  176. package/dist/src/tools/definitions/types.d.ts +2 -0
  177. package/dist/src/tools/get-internal-docs.js +5 -2
  178. package/dist/src/tools/get-internal-docs.js.map +1 -1
  179. package/dist/src/tools/list-mcp-resources.d.ts +24 -0
  180. package/dist/src/tools/list-mcp-resources.js +74 -0
  181. package/dist/src/tools/list-mcp-resources.js.map +1 -0
  182. package/dist/src/tools/list-mcp-resources.test.d.ts +6 -0
  183. package/dist/src/tools/list-mcp-resources.test.js +79 -0
  184. package/dist/src/tools/list-mcp-resources.test.js.map +1 -0
  185. package/dist/src/tools/mcp-client-manager.d.ts +3 -1
  186. package/dist/src/tools/mcp-client-manager.js +24 -1
  187. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  188. package/dist/src/tools/mcp-client-manager.test.js +43 -0
  189. package/dist/src/tools/mcp-client-manager.test.js.map +1 -1
  190. package/dist/src/tools/memoryTool.d.ts +2 -1
  191. package/dist/src/tools/memoryTool.js +42 -13
  192. package/dist/src/tools/memoryTool.js.map +1 -1
  193. package/dist/src/tools/memoryTool.test.js +23 -3
  194. package/dist/src/tools/memoryTool.test.js.map +1 -1
  195. package/dist/src/tools/read-mcp-resource.d.ts +25 -0
  196. package/dist/src/tools/read-mcp-resource.js +120 -0
  197. package/dist/src/tools/read-mcp-resource.js.map +1 -0
  198. package/dist/src/tools/read-mcp-resource.test.d.ts +6 -0
  199. package/dist/src/tools/read-mcp-resource.test.js +110 -0
  200. package/dist/src/tools/read-mcp-resource.test.js.map +1 -0
  201. package/dist/src/tools/ripGrep.d.ts +3 -2
  202. package/dist/src/tools/ripGrep.js +25 -54
  203. package/dist/src/tools/ripGrep.js.map +1 -1
  204. package/dist/src/tools/ripGrep.test.js +73 -131
  205. package/dist/src/tools/ripGrep.test.js.map +1 -1
  206. package/dist/src/tools/shell.js +38 -12
  207. package/dist/src/tools/shell.js.map +1 -1
  208. package/dist/src/tools/shell.test.js +410 -25
  209. package/dist/src/tools/shell.test.js.map +1 -1
  210. package/dist/src/tools/tool-error.d.ts +1 -0
  211. package/dist/src/tools/tool-error.js +1 -0
  212. package/dist/src/tools/tool-error.js.map +1 -1
  213. package/dist/src/tools/tool-names.d.ts +4 -4
  214. package/dist/src/tools/tool-names.js +6 -2
  215. package/dist/src/tools/tool-names.js.map +1 -1
  216. package/dist/src/tools/tool-registry.js +8 -1
  217. package/dist/src/tools/tool-registry.js.map +1 -1
  218. package/dist/src/utils/filesearch/fileSearch.d.ts +2 -0
  219. package/dist/src/utils/filesearch/fileSearch.js +97 -6
  220. package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
  221. package/dist/src/utils/filesearch/fileSearch.test.js +54 -0
  222. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
  223. package/dist/src/utils/filesearch/fileWatcher.d.ts +25 -0
  224. package/dist/src/utils/filesearch/fileWatcher.js +86 -0
  225. package/dist/src/utils/filesearch/fileWatcher.js.map +1 -0
  226. package/dist/src/utils/filesearch/fileWatcher.test.d.ts +6 -0
  227. package/dist/src/utils/filesearch/fileWatcher.test.js +142 -0
  228. package/dist/src/utils/filesearch/fileWatcher.test.js.map +1 -0
  229. package/dist/src/utils/gitIgnoreParser.js +1 -1
  230. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  231. package/dist/src/utils/ignoreFileParser.js +1 -1
  232. package/dist/src/utils/ignoreFileParser.js.map +1 -1
  233. package/dist/src/utils/memoryDiscovery.js +15 -5
  234. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  235. package/dist/src/utils/retry.js +18 -6
  236. package/dist/src/utils/retry.js.map +1 -1
  237. package/dist/src/utils/retry.test.js +30 -0
  238. package/dist/src/utils/retry.test.js.map +1 -1
  239. package/dist/src/utils/shell-utils.d.ts +1 -0
  240. package/dist/src/utils/shell-utils.js +106 -0
  241. package/dist/src/utils/shell-utils.js.map +1 -1
  242. package/dist/tsconfig.tsbuildinfo +1 -1
  243. package/package.json +5 -3
  244. package/vendor/ripgrep/rg-darwin-arm64 +0 -0
  245. package/vendor/ripgrep/rg-darwin-x64 +0 -0
  246. package/vendor/ripgrep/rg-linux-arm64 +0 -0
  247. package/vendor/ripgrep/rg-linux-x64 +0 -0
  248. package/vendor/ripgrep/rg-win32-x64.exe +0 -0
  249. package/dist/docs/get-started/installation.md +0 -181
  250. package/dist/google-gemini-cli-core-0.39.0-preview.1.tgz +0 -0
  251. package/dist/src/agents/memory-manager-agent.d.ts +0 -25
  252. package/dist/src/agents/memory-manager-agent.js +0 -138
  253. package/dist/src/agents/memory-manager-agent.js.map +0 -1
  254. package/dist/src/agents/memory-manager-agent.test.js +0 -123
  255. package/dist/src/agents/memory-manager-agent.test.js.map +0 -1
  256. package/dist/src/prompts/snippets-memory-manager.test.js +0 -31
  257. package/dist/src/prompts/snippets-memory-manager.test.js.map +0 -1
  258. /package/dist/src/agents/{memory-manager-agent.test.d.ts → skill-extraction-agent.test.d.ts} +0 -0
  259. /package/dist/src/prompts/{snippets-memory-manager.test.d.ts → snippets-memory-v2.test.d.ts} +0 -0
@@ -61,6 +61,7 @@ describe('ShellTool', () => {
61
61
  let mockShellOutputCallback;
62
62
  let resolveExecutionPromise;
63
63
  let tempRootDir;
64
+ let extractedTmpFile;
64
65
  beforeEach(() => {
65
66
  vi.clearAllMocks();
66
67
  tempRootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shell-test-'));
@@ -146,9 +147,14 @@ describe('ShellTool', () => {
146
147
  vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from('abcdef', 'hex'));
147
148
  process.env['ComSpec'] =
148
149
  'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
150
+ extractedTmpFile = '';
149
151
  // Capture the output callback to simulate streaming events from the service
150
- mockShellExecutionService.mockImplementation((_cmd, _cwd, callback) => {
152
+ mockShellExecutionService.mockImplementation((cmd, _cwd, callback) => {
151
153
  mockShellOutputCallback = callback;
154
+ const match = cmd.match(/pgrep -g 0 >([^ ]+)/);
155
+ if (match) {
156
+ extractedTmpFile = match[1].replace(/['"]/g, '');
157
+ }
152
158
  return {
153
159
  pid: 12345,
154
160
  result: new Promise((resolve) => {
@@ -227,38 +233,32 @@ describe('ShellTool', () => {
227
233
  it('should wrap command on linux and parse pgrep output', async () => {
228
234
  const invocation = shellTool.build({ command: 'my-command &' });
229
235
  const promise = invocation.execute({ abortSignal: mockAbortSignal });
230
- resolveShellExecution({ pid: 54321 });
231
236
  // Simulate pgrep output file creation by the shell command
232
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
233
- fs.writeFileSync(tmpFile, `54321${os.EOL}54322${os.EOL}`);
237
+ fs.writeFileSync(extractedTmpFile, `54321${os.EOL}54322${os.EOL}`);
238
+ resolveShellExecution({ pid: 54321 });
234
239
  const result = await promise;
235
- const wrappedCommand = `(\n${'my-command &'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
236
- expect(mockShellExecutionService).toHaveBeenCalledWith(wrappedCommand, tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
240
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
237
241
  pager: 'cat',
238
242
  sanitizationConfig: {},
239
243
  sandboxManager: expect.any(Object),
240
244
  }));
241
245
  expect(result.llmContent).toContain('Background PIDs: 54322');
242
246
  // The file should be deleted by the tool
243
- expect(fs.existsSync(tmpFile)).toBe(false);
247
+ expect(fs.existsSync(extractedTmpFile)).toBe(false);
244
248
  });
245
249
  it('should add a space when command ends with a backslash to prevent escaping newline', async () => {
246
250
  const invocation = shellTool.build({ command: 'ls\\' });
247
251
  const promise = invocation.execute({ abortSignal: mockAbortSignal });
248
252
  resolveShellExecution();
249
253
  await promise;
250
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
251
- const wrappedCommand = `(\nls\\ \n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
252
- expect(mockShellExecutionService).toHaveBeenCalledWith(wrappedCommand, tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.any(Object));
254
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.any(Object));
253
255
  });
254
256
  it('should handle trailing comments correctly by placing them on their own line', async () => {
255
257
  const invocation = shellTool.build({ command: 'ls # comment' });
256
258
  const promise = invocation.execute({ abortSignal: mockAbortSignal });
257
259
  resolveShellExecution();
258
260
  await promise;
259
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
260
- const wrappedCommand = `(\nls # comment\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
261
- expect(mockShellExecutionService).toHaveBeenCalledWith(wrappedCommand, tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.any(Object));
261
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.any(Object));
262
262
  });
263
263
  it('should use the provided absolute directory as cwd', async () => {
264
264
  const subdir = path.join(tempRootDir, 'subdir');
@@ -269,9 +269,7 @@ describe('ShellTool', () => {
269
269
  const promise = invocation.execute({ abortSignal: mockAbortSignal });
270
270
  resolveShellExecution();
271
271
  await promise;
272
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
273
- const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
274
- expect(mockShellExecutionService).toHaveBeenCalledWith(wrappedCommand, subdir, expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
272
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), subdir, expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
275
273
  pager: 'cat',
276
274
  sanitizationConfig: {},
277
275
  sandboxManager: expect.any(Object),
@@ -285,9 +283,7 @@ describe('ShellTool', () => {
285
283
  const promise = invocation.execute({ abortSignal: mockAbortSignal });
286
284
  resolveShellExecution();
287
285
  await promise;
288
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
289
- const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
290
- expect(mockShellExecutionService).toHaveBeenCalledWith(wrappedCommand, path.join(tempRootDir, 'subdir'), expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
286
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), path.join(tempRootDir, 'subdir'), expect.any(Function), expect.any(AbortSignal), false, expect.objectContaining({
291
287
  pager: 'cat',
292
288
  sanitizationConfig: {},
293
289
  sandboxManager: expect.any(Object),
@@ -328,6 +324,17 @@ describe('ShellTool', () => {
328
324
  sandboxManager: expect.any(NoopSandboxManager),
329
325
  }));
330
326
  }, 20000);
327
+ it('should correctly wrap heredoc commands', async () => {
328
+ const command = `cat << 'EOF'
329
+ hello world
330
+ EOF`;
331
+ const invocation = shellTool.build({ command });
332
+ const promise = invocation.execute({ abortSignal: mockAbortSignal });
333
+ resolveShellExecution();
334
+ await promise;
335
+ expect(mockShellExecutionService).toHaveBeenCalledWith(expect.stringMatching(/pgrep -g 0 >.*gemini-shell-.*[/\\]pgrep\.tmp/), tempRootDir, expect.any(Function), expect.any(AbortSignal), false, expect.any(Object));
336
+ expect(mockShellExecutionService.mock.calls[0][0]).toMatch(/\nEOF\n\)\n/);
337
+ });
331
338
  it('should format error messages correctly', async () => {
332
339
  const error = new Error('wrapped command failed');
333
340
  const invocation = shellTool.build({ command: 'user-command' });
@@ -403,16 +410,18 @@ describe('ShellTool', () => {
403
410
  });
404
411
  it('should clean up the temp file on synchronous execution error', async () => {
405
412
  const error = new Error('sync spawn error');
406
- mockShellExecutionService.mockImplementation(() => {
407
- // Create the temp file before throwing to simulate it being left behind
408
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
409
- fs.writeFileSync(tmpFile, '');
413
+ mockShellExecutionService.mockImplementation((cmd) => {
414
+ const match = cmd.match(/pgrep -g 0 >([^ ]+)/);
415
+ if (match) {
416
+ extractedTmpFile = match[1].replace(/['"]/g, ''); // remove any quotes if present
417
+ // Create the temp file before throwing to simulate it being left behind
418
+ fs.writeFileSync(extractedTmpFile, '');
419
+ }
410
420
  throw error;
411
421
  });
412
422
  const invocation = shellTool.build({ command: 'a-command' });
413
423
  await expect(invocation.execute({ abortSignal: mockAbortSignal })).rejects.toThrow(error);
414
- const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
415
- expect(fs.existsSync(tmpFile)).toBe(false);
424
+ expect(fs.existsSync(extractedTmpFile)).toBe(false);
416
425
  });
417
426
  it('should not log "missing pgrep output" when process is backgrounded', async () => {
418
427
  vi.useFakeTimers();
@@ -881,5 +890,381 @@ describe('ShellTool', () => {
881
890
  expect(schema.description).toMatchSnapshot();
882
891
  });
883
892
  });
893
+ describe('command injection detection', () => {
894
+ it('should block $() command substitution', async () => {
895
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
896
+ const invocation = tool.build({ command: 'echo $(whoami)' });
897
+ const result = await invocation.execute({
898
+ abortSignal: new AbortController().signal,
899
+ });
900
+ expect(result.returnDisplay).toContain('Blocked');
901
+ });
902
+ it('should block backtick command substitution', async () => {
903
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
904
+ const invocation = tool.build({ command: 'echo `whoami`' });
905
+ const result = await invocation.execute({
906
+ abortSignal: new AbortController().signal,
907
+ });
908
+ expect(result.returnDisplay).toContain('Blocked');
909
+ });
910
+ it('should allow normal commands without substitution', async () => {
911
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
912
+ pid: 12345,
913
+ result: Promise.resolve({
914
+ output: 'hello',
915
+ rawOutput: Buffer.from('hello'),
916
+ exitCode: 0,
917
+ signal: null,
918
+ error: null,
919
+ aborted: false,
920
+ pid: 12345,
921
+ executionMethod: 'child_process',
922
+ backgrounded: false,
923
+ }),
924
+ }));
925
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
926
+ const invocation = tool.build({ command: 'echo hello' });
927
+ const result = await invocation.execute({
928
+ abortSignal: new AbortController().signal,
929
+ });
930
+ expect(result.returnDisplay).not.toContain('Blocked');
931
+ });
932
+ it('should allow single quoted strings with special chars', async () => {
933
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
934
+ pid: 12345,
935
+ result: Promise.resolve({
936
+ output: '$(not substituted)',
937
+ rawOutput: Buffer.from('$(not substituted)'),
938
+ exitCode: 0,
939
+ signal: null,
940
+ error: null,
941
+ aborted: false,
942
+ pid: 12345,
943
+ executionMethod: 'child_process',
944
+ backgrounded: false,
945
+ }),
946
+ }));
947
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
948
+ const invocation = tool.build({
949
+ command: "echo '$(not substituted)'",
950
+ });
951
+ const result = await invocation.execute({
952
+ abortSignal: new AbortController().signal,
953
+ });
954
+ expect(result.returnDisplay).not.toContain('Blocked');
955
+ });
956
+ it('should allow escaped backtick outside double quotes', async () => {
957
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
958
+ pid: 12345,
959
+ result: Promise.resolve({
960
+ output: 'hello',
961
+ rawOutput: Buffer.from('hello'),
962
+ exitCode: 0,
963
+ signal: null,
964
+ error: null,
965
+ aborted: false,
966
+ pid: 12345,
967
+ executionMethod: 'child_process',
968
+ backgrounded: false,
969
+ }),
970
+ }));
971
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
972
+ const invocation = tool.build({ command: 'echo \\`hello\\`' });
973
+ const result = await invocation.execute({
974
+ abortSignal: new AbortController().signal,
975
+ });
976
+ expect(result.returnDisplay).not.toContain('Blocked');
977
+ });
978
+ it('should block $() inside double quotes', async () => {
979
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
980
+ const invocation = tool.build({ command: 'echo "$(whoami)"' });
981
+ const result = await invocation.execute({
982
+ abortSignal: new AbortController().signal,
983
+ });
984
+ expect(result.returnDisplay).toContain('Blocked');
985
+ });
986
+ it('should block >() process substitution', async () => {
987
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
988
+ const invocation = tool.build({ command: 'echo >(whoami)' });
989
+ const result = await invocation.execute({
990
+ abortSignal: new AbortController().signal,
991
+ });
992
+ expect(result.returnDisplay).toContain('Blocked');
993
+ });
994
+ it('should allow $() inside single quotes', async () => {
995
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
996
+ pid: 12345,
997
+ result: Promise.resolve({
998
+ output: '$(whoami)',
999
+ rawOutput: Buffer.from('$(whoami)'),
1000
+ exitCode: 0,
1001
+ signal: null,
1002
+ error: null,
1003
+ aborted: false,
1004
+ pid: 12345,
1005
+ executionMethod: 'child_process',
1006
+ backgrounded: false,
1007
+ }),
1008
+ }));
1009
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1010
+ const invocation = tool.build({
1011
+ command: "echo '$(whoami)'",
1012
+ });
1013
+ const result = await invocation.execute({
1014
+ abortSignal: new AbortController().signal,
1015
+ });
1016
+ expect(result.returnDisplay).not.toContain('Blocked');
1017
+ });
1018
+ it('should block PowerShell @() array subexpression', async () => {
1019
+ mockPlatform.mockReturnValue('win32');
1020
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1021
+ const invocation = tool.build({ command: 'echo @(whoami)' });
1022
+ const result = await invocation.execute({
1023
+ abortSignal: new AbortController().signal,
1024
+ });
1025
+ expect(result.returnDisplay).toContain('Blocked');
1026
+ });
1027
+ it('should block PowerShell $() subexpression', async () => {
1028
+ mockPlatform.mockReturnValue('win32');
1029
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1030
+ const invocation = tool.build({ command: 'echo $(whoami)' });
1031
+ const result = await invocation.execute({
1032
+ abortSignal: new AbortController().signal,
1033
+ });
1034
+ expect(result.returnDisplay).toContain('Blocked');
1035
+ });
1036
+ it('should allow PowerShell single quoted strings', async () => {
1037
+ mockPlatform.mockReturnValue('win32');
1038
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1039
+ pid: 12345,
1040
+ result: Promise.resolve({
1041
+ output: '$(whoami)',
1042
+ rawOutput: Buffer.from('$(whoami)'),
1043
+ exitCode: 0,
1044
+ signal: null,
1045
+ error: null,
1046
+ aborted: false,
1047
+ pid: 12345,
1048
+ executionMethod: 'child_process',
1049
+ backgrounded: false,
1050
+ }),
1051
+ }));
1052
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1053
+ const invocation = tool.build({
1054
+ command: "echo '$(whoami)'",
1055
+ });
1056
+ const result = await invocation.execute({
1057
+ abortSignal: new AbortController().signal,
1058
+ });
1059
+ expect(result.returnDisplay).not.toContain('Blocked');
1060
+ });
1061
+ it('should allow escaped substitution outside quotes', async () => {
1062
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1063
+ pid: 12345,
1064
+ result: Promise.resolve({
1065
+ output: '$(whoami)',
1066
+ rawOutput: Buffer.from('$(whoami)'),
1067
+ exitCode: 0,
1068
+ signal: null,
1069
+ error: null,
1070
+ aborted: false,
1071
+ pid: 12345,
1072
+ executionMethod: 'child_process',
1073
+ backgrounded: false,
1074
+ }),
1075
+ }));
1076
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1077
+ const invocation = tool.build({ command: 'echo \\$(whoami)' });
1078
+ const result = await invocation.execute({
1079
+ abortSignal: new AbortController().signal,
1080
+ });
1081
+ expect(result.returnDisplay).not.toContain('Blocked');
1082
+ });
1083
+ it('should allow process substitution inside double quotes', async () => {
1084
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1085
+ pid: 12345,
1086
+ result: Promise.resolve({
1087
+ output: '<(whoami)',
1088
+ rawOutput: Buffer.from('<(whoami)'),
1089
+ exitCode: 0,
1090
+ signal: null,
1091
+ error: null,
1092
+ aborted: false,
1093
+ pid: 12345,
1094
+ executionMethod: 'child_process',
1095
+ backgrounded: false,
1096
+ }),
1097
+ }));
1098
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1099
+ const invocation = tool.build({ command: 'echo "<(whoami)"' });
1100
+ const result = await invocation.execute({
1101
+ abortSignal: new AbortController().signal,
1102
+ });
1103
+ expect(result.returnDisplay).not.toContain('Blocked');
1104
+ });
1105
+ it('should block process substitution without quotes', async () => {
1106
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1107
+ const invocation = tool.build({ command: 'echo <(whoami)' });
1108
+ const result = await invocation.execute({
1109
+ abortSignal: new AbortController().signal,
1110
+ });
1111
+ expect(result.returnDisplay).toContain('Blocked');
1112
+ });
1113
+ it('should allow escaped $() outside double quotes', async () => {
1114
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1115
+ pid: 12345,
1116
+ result: Promise.resolve({
1117
+ output: '$(whoami)',
1118
+ rawOutput: Buffer.from('$(whoami)'),
1119
+ exitCode: 0,
1120
+ signal: null,
1121
+ error: null,
1122
+ aborted: false,
1123
+ pid: 12345,
1124
+ executionMethod: 'child_process',
1125
+ backgrounded: false,
1126
+ }),
1127
+ }));
1128
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1129
+ const invocation = tool.build({ command: 'echo \\$(whoami)' });
1130
+ const result = await invocation.execute({
1131
+ abortSignal: new AbortController().signal,
1132
+ });
1133
+ expect(result.returnDisplay).not.toContain('Blocked');
1134
+ });
1135
+ it('should allow output process substitution inside double quotes', async () => {
1136
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1137
+ pid: 12345,
1138
+ result: Promise.resolve({
1139
+ output: '<(whoami)',
1140
+ rawOutput: Buffer.from('<(whoami)'),
1141
+ exitCode: 0,
1142
+ signal: null,
1143
+ error: null,
1144
+ aborted: false,
1145
+ pid: 12345,
1146
+ executionMethod: 'child_process',
1147
+ backgrounded: false,
1148
+ }),
1149
+ }));
1150
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1151
+ const invocation = tool.build({ command: 'echo "<(whoami)"' });
1152
+ const result = await invocation.execute({
1153
+ abortSignal: new AbortController().signal,
1154
+ });
1155
+ expect(result.returnDisplay).not.toContain('Blocked');
1156
+ });
1157
+ it('should block <() process substitution without quotes', async () => {
1158
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1159
+ const invocation = tool.build({ command: 'echo <(whoami)' });
1160
+ const result = await invocation.execute({
1161
+ abortSignal: new AbortController().signal,
1162
+ });
1163
+ expect(result.returnDisplay).toContain('Blocked');
1164
+ });
1165
+ it('should block PowerShell bare () grouping operator', async () => {
1166
+ mockPlatform.mockReturnValue('win32');
1167
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1168
+ const invocation = tool.build({ command: 'echo (whoami)' });
1169
+ const result = await invocation.execute({
1170
+ abortSignal: new AbortController().signal,
1171
+ });
1172
+ expect(result.returnDisplay).toContain('Blocked');
1173
+ });
1174
+ it('should allow escaped $() inside double quotes', async () => {
1175
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1176
+ pid: 12345,
1177
+ result: Promise.resolve({
1178
+ output: '$(whoami)',
1179
+ rawOutput: Buffer.from('$(whoami)'),
1180
+ exitCode: 0,
1181
+ signal: null,
1182
+ error: null,
1183
+ aborted: false,
1184
+ pid: 12345,
1185
+ executionMethod: 'child_process',
1186
+ backgrounded: false,
1187
+ }),
1188
+ }));
1189
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1190
+ const invocation = tool.build({ command: 'echo "\\$(whoami)"' });
1191
+ const result = await invocation.execute({
1192
+ abortSignal: new AbortController().signal,
1193
+ });
1194
+ expect(result.returnDisplay).not.toContain('Blocked');
1195
+ });
1196
+ it('should allow escaped substitution inside double quotes', async () => {
1197
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1198
+ pid: 12345,
1199
+ result: Promise.resolve({
1200
+ output: '$(whoami)',
1201
+ rawOutput: Buffer.from('$(whoami)'),
1202
+ exitCode: 0,
1203
+ signal: null,
1204
+ error: null,
1205
+ aborted: false,
1206
+ pid: 12345,
1207
+ executionMethod: 'child_process',
1208
+ backgrounded: false,
1209
+ }),
1210
+ }));
1211
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1212
+ const invocation = tool.build({ command: 'echo "\\$(whoami)"' });
1213
+ const result = await invocation.execute({
1214
+ abortSignal: new AbortController().signal,
1215
+ });
1216
+ expect(result.returnDisplay).not.toContain('Blocked');
1217
+ });
1218
+ it('should allow PowerShell keyword with flag e.g. switch -regex ($x)', async () => {
1219
+ mockPlatform.mockReturnValue('win32');
1220
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1221
+ pid: 12345,
1222
+ result: Promise.resolve({
1223
+ output: 'result',
1224
+ rawOutput: Buffer.from('result'),
1225
+ exitCode: 0,
1226
+ signal: null,
1227
+ error: null,
1228
+ aborted: false,
1229
+ pid: 12345,
1230
+ executionMethod: 'child_process',
1231
+ backgrounded: false,
1232
+ }),
1233
+ }));
1234
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1235
+ const invocation = tool.build({
1236
+ command: 'switch -regex ($x) { "a" { 1 } }',
1237
+ });
1238
+ const result = await invocation.execute({
1239
+ abortSignal: new AbortController().signal,
1240
+ });
1241
+ expect(result.returnDisplay).not.toContain('Blocked');
1242
+ });
1243
+ it('should allow PowerShell nested parentheses e.g. if ((condition))', async () => {
1244
+ mockPlatform.mockReturnValue('win32');
1245
+ mockShellExecutionService.mockImplementation((_cmd, _cwd, _callback) => ({
1246
+ pid: 12345,
1247
+ result: Promise.resolve({
1248
+ output: 'result',
1249
+ rawOutput: Buffer.from('result'),
1250
+ exitCode: 0,
1251
+ signal: null,
1252
+ error: null,
1253
+ aborted: false,
1254
+ pid: 12345,
1255
+ executionMethod: 'child_process',
1256
+ backgrounded: false,
1257
+ }),
1258
+ }));
1259
+ const tool = new ShellTool(mockConfig, createMockMessageBus());
1260
+ const invocation = tool.build({
1261
+ command: 'if ((condition)) { Write-Host ok }',
1262
+ });
1263
+ const result = await invocation.execute({
1264
+ abortSignal: new AbortController().signal,
1265
+ });
1266
+ expect(result.returnDisplay).not.toContain('Blocked');
1267
+ });
1268
+ });
884
1269
  });
885
1270
  //# sourceMappingURL=shell.test.js.map