@animus-labs/cortex 0.2.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 (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Bash tool: execute shell commands in the host environment.
3
+ *
4
+ * Cross-platform: bash/zsh on macOS and Linux, PowerShell on Windows.
5
+ * Tracks working directory across calls within an agentic loop.
6
+ * Supports background execution with auto-yield.
7
+ *
8
+ * Reference: docs/cortex/tools/bash.md
9
+ */
10
+
11
+ import * as child_process from 'node:child_process';
12
+ import * as fs from 'node:fs';
13
+ import { Type, type Static } from 'typebox';
14
+ import type { CwdTracker } from '../shared/cwd-tracker.js';
15
+ import type { ToolContentDetails, ToolExecuteContext } from '../../types.js';
16
+ import { buildSafeEnv, runSafetyChecks } from './safety.js';
17
+ import {
18
+ type BackgroundTask,
19
+ type CortexToolRuntime,
20
+ attachRuntimeAwareTool,
21
+ globalBackgroundTaskStore,
22
+ } from '../runtime.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Schema
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export const BashParams = Type.Object({
29
+ command: Type.String({ description: 'The shell command to execute' }),
30
+ timeout: Type.Optional(
31
+ Type.Number({ description: 'Timeout in milliseconds. Default: 120000 (2 min). Max: 600000 (10 min).' }),
32
+ ),
33
+ description: Type.Optional(
34
+ Type.String({ description: 'Human-readable explanation of the command.' }),
35
+ ),
36
+ background: Type.Optional(
37
+ Type.Boolean({ description: 'Run the command in the background immediately. Default: false.' }),
38
+ ),
39
+ });
40
+
41
+ export type BashParamsType = Static<typeof BashParams>;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Details type
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export interface BashDetails {
48
+ stdout: string;
49
+ stderr: string;
50
+ exitCode: number | null;
51
+ duration: number;
52
+ interrupted: boolean;
53
+ timedOut: boolean;
54
+ backgrounded: boolean;
55
+ taskId: string | null;
56
+ finalCwd: string;
57
+ }
58
+
59
+ /**
60
+ * Partial result details emitted during bash streaming via onUpdate.
61
+ */
62
+ export interface BashStreamUpdate {
63
+ /** New stdout chunk since last update (complete lines only). */
64
+ stdout: string;
65
+ /** New stderr chunk since last update. */
66
+ stderr: string;
67
+ /** Total stdout lines emitted so far. */
68
+ totalLines: number;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Constants
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const DEFAULT_TIMEOUT = 120_000;
76
+ const MAX_TIMEOUT = 600_000;
77
+ const AUTO_YIELD_THRESHOLD = 10_000; // 10 seconds
78
+ const CWD_MARKER = '___CWD___';
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Config
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export interface BashToolConfig {
85
+ runtime?: CortexToolRuntime | undefined;
86
+ cwdTracker?: CwdTracker | undefined;
87
+ /** Custom shell path override. */
88
+ shellPath?: string | undefined;
89
+ /** Auto-yield threshold in ms. Default: 10000. */
90
+ autoYieldThreshold?: number | undefined;
91
+ /** Callback for tracking subprocess PIDs (for cleanup on exit). */
92
+ onProcessSpawned?: ((pid: number) => void) | undefined;
93
+ /** Callback for removing tracked PIDs when process exits. */
94
+ onProcessExited?: ((pid: number) => void) | undefined;
95
+ /** Utility model completion function for Layer 7 safety classifier. */
96
+ utilityComplete?: ((context: unknown) => Promise<unknown>) | undefined;
97
+ /**
98
+ * Consumer-set environment variable overrides that bypass the security blocklist.
99
+ * Merged ON TOP of the sanitized environment for shell subprocesses.
100
+ * Used for macOS dock icon suppression vars (DYLD_INSERT_LIBRARIES, etc.).
101
+ */
102
+ envOverrides?: Record<string, string> | undefined;
103
+ }
104
+
105
+ export function getBackgroundTask(id: string): BackgroundTask | undefined {
106
+ return globalBackgroundTaskStore.get(id);
107
+ }
108
+
109
+ export function getAllBackgroundTasks(): Map<string, BackgroundTask> {
110
+ return globalBackgroundTaskStore.getAll();
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Shell Selection
115
+ // ---------------------------------------------------------------------------
116
+
117
+ interface ShellConfig {
118
+ shell: string;
119
+ args: string[];
120
+ }
121
+
122
+ /**
123
+ * Read /etc/shells and return the set of trusted shell paths.
124
+ */
125
+ function readTrustedShells(): Set<string> {
126
+ const trusted = new Set<string>();
127
+ try {
128
+ const content = fs.readFileSync('/etc/shells', 'utf8');
129
+ for (const line of content.split('\n')) {
130
+ const trimmed = line.trim();
131
+ if (trimmed && !trimmed.startsWith('#')) {
132
+ trusted.add(trimmed);
133
+ }
134
+ }
135
+ } catch {
136
+ // /etc/shells not available; empty set means we fall back
137
+ }
138
+ return trusted;
139
+ }
140
+
141
+ /**
142
+ * Select the appropriate shell for the current platform.
143
+ */
144
+ function selectShell(customShellPath?: string): ShellConfig {
145
+ // Custom override
146
+ if (customShellPath) {
147
+ if (process.platform === 'win32') {
148
+ return { shell: customShellPath, args: ['-NoProfile', '-NonInteractive', '-Command'] };
149
+ }
150
+ return { shell: customShellPath, args: ['-c'] };
151
+ }
152
+
153
+ if (process.platform === 'win32') {
154
+ return selectWindowsShell();
155
+ }
156
+
157
+ return selectUnixShell();
158
+ }
159
+
160
+ function selectUnixShell(): ShellConfig {
161
+ const userShell = process.env['SHELL'];
162
+
163
+ if (userShell) {
164
+ // Reject fish (incompatible with common bashisms)
165
+ if (userShell.endsWith('/fish')) {
166
+ return findUnixFallback();
167
+ }
168
+
169
+ // Validate against /etc/shells
170
+ const trusted = readTrustedShells();
171
+ if (trusted.size === 0 || trusted.has(userShell)) {
172
+ return { shell: userShell, args: ['-c'] };
173
+ }
174
+ }
175
+
176
+ return findUnixFallback();
177
+ }
178
+
179
+ function findUnixFallback(): ShellConfig {
180
+ // Try /bin/bash first, then /bin/sh
181
+ for (const shell of ['/bin/bash', '/bin/sh']) {
182
+ try {
183
+ fs.accessSync(shell, fs.constants.X_OK);
184
+ return { shell, args: ['-c'] };
185
+ } catch {
186
+ continue;
187
+ }
188
+ }
189
+
190
+ return { shell: '/bin/sh', args: ['-c'] };
191
+ }
192
+
193
+ function selectWindowsShell(): ShellConfig {
194
+ // Try PowerShell 7 first
195
+ const ps7Paths = [
196
+ 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
197
+ `${process.env['ProgramW6432']}\\PowerShell\\7\\pwsh.exe`,
198
+ ];
199
+
200
+ for (const ps7 of ps7Paths) {
201
+ try {
202
+ fs.accessSync(ps7, fs.constants.X_OK);
203
+ return { shell: ps7, args: ['-NoProfile', '-NonInteractive', '-Command'] };
204
+ } catch {
205
+ continue;
206
+ }
207
+ }
208
+
209
+ // Fall back to Windows PowerShell 5.1
210
+ const ps5 = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
211
+ return { shell: ps5, args: ['-NoProfile', '-NonInteractive', '-Command'] };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Output handling
216
+ // ---------------------------------------------------------------------------
217
+
218
+ /**
219
+ * Sanitize output by stripping binary control characters.
220
+ * Preserves tab, newline, and carriage return.
221
+ */
222
+ function sanitizeOutput(output: string): string {
223
+ // eslint-disable-next-line no-control-regex
224
+ return output.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
225
+ }
226
+
227
+ /**
228
+ * Extract CWD from output using the CWD_MARKER.
229
+ * Returns [cleanedOutput, extractedCwd].
230
+ */
231
+ function extractCwd(output: string): [string, string | null] {
232
+ const markerIdx = output.lastIndexOf(CWD_MARKER);
233
+ if (markerIdx === -1) return [output, null];
234
+
235
+ const beforeMarker = output.slice(0, markerIdx);
236
+ const afterMarker = output.slice(markerIdx + CWD_MARKER.length).trim();
237
+
238
+ // The CWD is on the line after the marker
239
+ const lines = afterMarker.split('\n');
240
+ const cwd = (lines[0] ?? '').trim();
241
+
242
+ return [beforeMarker.trimEnd(), cwd || null];
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Tool factory
247
+ // ---------------------------------------------------------------------------
248
+
249
+ export function createBashTool(config: BashToolConfig): {
250
+ name: string;
251
+ description: string;
252
+ parameters: typeof BashParams;
253
+ execute: (params: BashParamsType, context?: ToolExecuteContext) => Promise<ToolContentDetails<BashDetails>>;
254
+ } {
255
+ const cwdTracker = config.runtime?.cwdTracker ?? config.cwdTracker;
256
+ if (!cwdTracker) {
257
+ throw new Error('createBashTool requires either runtime or cwdTracker');
258
+ }
259
+ const backgroundTasks = config.runtime?.backgroundTasks ?? globalBackgroundTaskStore;
260
+ const autoYieldThreshold = config.autoYieldThreshold ?? AUTO_YIELD_THRESHOLD;
261
+
262
+ const tool = {
263
+ name: 'Bash',
264
+ description: 'Execute a shell command in the host environment.',
265
+ parameters: BashParams,
266
+
267
+ async execute(params: BashParamsType, context?: ToolExecuteContext): Promise<ToolContentDetails<BashDetails>> {
268
+ backgroundTasks.cleanupCompletedTasks();
269
+ const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
270
+ const background = params.background ?? false;
271
+ const startTime = Date.now();
272
+
273
+ // Run safety checks (Layers 2-7)
274
+ const safetyResult = await runSafetyChecks(
275
+ params.command,
276
+ cwdTracker.getDefaultDir(),
277
+ cwdTracker.getCwd(),
278
+ {
279
+ utilityComplete: config.utilityComplete,
280
+ description: params.description,
281
+ },
282
+ );
283
+
284
+ if (!safetyResult.allowed) {
285
+ return {
286
+ content: [{ type: 'text', text: safetyResult.reason ?? 'Command blocked by safety check.' }],
287
+ details: {
288
+ stdout: '',
289
+ stderr: '',
290
+ exitCode: null,
291
+ duration: Date.now() - startTime,
292
+ interrupted: false,
293
+ timedOut: false,
294
+ backgrounded: false,
295
+ taskId: null,
296
+ finalCwd: cwdTracker.getCwd(),
297
+ },
298
+ };
299
+ }
300
+
301
+ // Select shell
302
+ const shellConfig = selectShell(config.shellPath);
303
+
304
+ // Verify shell exists
305
+ try {
306
+ fs.accessSync(shellConfig.shell, fs.constants.X_OK);
307
+ } catch {
308
+ return {
309
+ content: [{ type: 'text', text: `Shell not found: ${shellConfig.shell}. Configure a custom shell in settings.` }],
310
+ details: {
311
+ stdout: '',
312
+ stderr: '',
313
+ exitCode: null,
314
+ duration: Date.now() - startTime,
315
+ interrupted: false,
316
+ timedOut: false,
317
+ backgrounded: false,
318
+ taskId: null,
319
+ finalCwd: cwdTracker.getCwd(),
320
+ },
321
+ };
322
+ }
323
+
324
+ // Build safe environment (Layer 1), with consumer overrides merged on top
325
+ const safeEnv = buildSafeEnv(process.env, config.envOverrides);
326
+
327
+ // Append CWD capture suffix
328
+ const isWindows = process.platform === 'win32';
329
+ // Capture exit code before CWD suffix so pwd/Get-Location don't mask it
330
+ const cwdSuffix = isWindows
331
+ ? `; $__ec=$LASTEXITCODE; Write-Host "${CWD_MARKER}"; Get-Location; exit $__ec`
332
+ : `; __ec=$?; echo "${CWD_MARKER}"; pwd; exit $__ec`;
333
+
334
+ // UTF-8 prefix for Windows PowerShell
335
+ const utf8Prefix = isWindows
336
+ ? '$OutputEncoding = [System.Text.Encoding]::UTF8; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; '
337
+ : '';
338
+
339
+ const fullCommand = `${utf8Prefix}${params.command}${cwdSuffix}`;
340
+
341
+ // Spawn the process
342
+ const proc = child_process.spawn(
343
+ shellConfig.shell,
344
+ [...shellConfig.args, fullCommand],
345
+ {
346
+ cwd: cwdTracker.getCwd(),
347
+ env: safeEnv,
348
+ stdio: ['pipe', 'pipe', 'pipe'],
349
+ detached: !isWindows, // Process group for Unix cleanup
350
+ },
351
+ );
352
+
353
+ // Track PID
354
+ if (proc.pid && config.onProcessSpawned) {
355
+ config.onProcessSpawned(proc.pid);
356
+ }
357
+
358
+ // Background execution
359
+ if (background) {
360
+ const taskId = backgroundTasks.nextTaskId();
361
+ const task: BackgroundTask = {
362
+ id: taskId,
363
+ command: params.command.slice(0, 120),
364
+ process: proc,
365
+ stdout: '',
366
+ stderr: '',
367
+ exitCode: null,
368
+ completed: false,
369
+ startTime: Date.now(),
370
+ };
371
+ backgroundTasks.set(task);
372
+
373
+ proc.stdout?.setEncoding('utf8');
374
+ proc.stderr?.setEncoding('utf8');
375
+ proc.stdout?.on('data', (data: string) => { task.stdout += data; });
376
+ proc.stderr?.on('data', (data: string) => { task.stderr += data; });
377
+ proc.on('close', (code) => {
378
+ task.exitCode = code;
379
+ task.completed = true;
380
+ if (proc.pid && config.onProcessExited) {
381
+ config.onProcessExited(proc.pid);
382
+ }
383
+ });
384
+
385
+ return {
386
+ content: [{ type: 'text', text: `Command running in background. Task ID: ${taskId}\nUse TaskOutput to poll, send input, or kill.` }],
387
+ details: {
388
+ stdout: '',
389
+ stderr: '',
390
+ exitCode: null,
391
+ duration: 0,
392
+ interrupted: false,
393
+ timedOut: false,
394
+ backgrounded: true,
395
+ taskId,
396
+ finalCwd: cwdTracker.getCwd(),
397
+ },
398
+ };
399
+ }
400
+
401
+ // Foreground execution
402
+ return new Promise<ToolContentDetails<BashDetails>>((resolve) => {
403
+ let stdout = '';
404
+ let stderr = '';
405
+ let timedOut = false;
406
+ let autoYielded = false;
407
+ let taskId: string | null = null;
408
+
409
+ // Streaming state: buffer chunks and emit complete lines every 100ms
410
+ const onUpdate = context?.onUpdate;
411
+ let pendingStdout = '';
412
+ let pendingStderr = '';
413
+ let totalLinesEmitted = 0;
414
+
415
+ const flushStreamUpdate = (): void => {
416
+ if (!onUpdate) return;
417
+ // Only emit complete lines (hold partial lines in the buffer)
418
+ const lastNewline = pendingStdout.lastIndexOf('\n');
419
+ const lastStderrNewline = pendingStderr.lastIndexOf('\n');
420
+ if (lastNewline === -1 && lastStderrNewline === -1) return;
421
+
422
+ let stdoutChunk = '';
423
+ if (lastNewline >= 0) {
424
+ stdoutChunk = pendingStdout.slice(0, lastNewline + 1);
425
+ pendingStdout = pendingStdout.slice(lastNewline + 1);
426
+ totalLinesEmitted += stdoutChunk.split('\n').length - 1;
427
+ }
428
+
429
+ let stderrChunk = '';
430
+ if (lastStderrNewline >= 0) {
431
+ stderrChunk = pendingStderr.slice(0, lastStderrNewline + 1);
432
+ pendingStderr = pendingStderr.slice(lastStderrNewline + 1);
433
+ }
434
+
435
+ onUpdate({
436
+ content: [{ type: 'text', text: stdoutChunk + stderrChunk }],
437
+ details: { stdout: stdoutChunk, stderr: stderrChunk, totalLines: totalLinesEmitted },
438
+ });
439
+ };
440
+
441
+ const streamInterval = onUpdate ? setInterval(flushStreamUpdate, 100) : null;
442
+
443
+ proc.stdout?.setEncoding('utf8');
444
+ proc.stderr?.setEncoding('utf8');
445
+ proc.stdout?.on('data', (data: string) => {
446
+ stdout += data;
447
+ pendingStdout += data;
448
+ });
449
+ proc.stderr?.on('data', (data: string) => {
450
+ stderr += data;
451
+ pendingStderr += data;
452
+ });
453
+
454
+ // Timeout handler
455
+ const timeoutTimer = setTimeout(() => {
456
+ timedOut = true;
457
+ killProcessTree(proc);
458
+ }, timeout);
459
+
460
+ // Auto-yield handler
461
+ const autoYieldTimer = setTimeout(() => {
462
+ if (!proc.exitCode && proc.pid) {
463
+ autoYielded = true;
464
+ taskId = backgroundTasks.nextTaskId();
465
+ const task: BackgroundTask = {
466
+ id: taskId,
467
+ command: params.command.slice(0, 120),
468
+ process: proc,
469
+ stdout,
470
+ stderr,
471
+ exitCode: null,
472
+ completed: false,
473
+ startTime: Date.now(),
474
+ };
475
+ backgroundTasks.set(task);
476
+
477
+ // Remove original foreground listeners to prevent memory leak
478
+ proc.stdout?.removeAllListeners('data');
479
+ proc.stderr?.removeAllListeners('data');
480
+ // Continue collecting output for the background task
481
+ proc.stdout?.on('data', (data: string) => { task.stdout += data; });
482
+ proc.stderr?.on('data', (data: string) => { task.stderr += data; });
483
+ proc.on('close', (code) => {
484
+ task.exitCode = code;
485
+ task.completed = true;
486
+ if (proc.pid && config.onProcessExited) {
487
+ config.onProcessExited(proc.pid);
488
+ }
489
+ });
490
+
491
+ clearTimeout(timeoutTimer);
492
+ if (streamInterval) {
493
+ clearInterval(streamInterval);
494
+ flushStreamUpdate();
495
+ }
496
+
497
+ const [cleanedOutput] = extractCwd(sanitizeOutput(stdout));
498
+ resolve({
499
+ content: [{ type: 'text', text: `${cleanedOutput}\n\n[Command auto-yielded after ${autoYieldThreshold}ms. Task ID: ${taskId}]` }],
500
+ details: {
501
+ stdout: cleanedOutput,
502
+ stderr,
503
+ exitCode: null,
504
+ duration: Date.now() - startTime,
505
+ interrupted: false,
506
+ timedOut: false,
507
+ backgrounded: true,
508
+ taskId,
509
+ finalCwd: cwdTracker.getCwd(),
510
+ },
511
+ });
512
+ }
513
+ }, autoYieldThreshold);
514
+
515
+ proc.on('close', (code) => {
516
+ clearTimeout(timeoutTimer);
517
+ clearTimeout(autoYieldTimer);
518
+ if (streamInterval) {
519
+ clearInterval(streamInterval);
520
+ flushStreamUpdate();
521
+ }
522
+
523
+ if (proc.pid && config.onProcessExited) {
524
+ config.onProcessExited(proc.pid);
525
+ }
526
+
527
+ // If already auto-yielded, don't resolve again
528
+ if (autoYielded) return;
529
+
530
+ const rawOutput = sanitizeOutput(stdout);
531
+ const [cleanedOutput, newCwd] = extractCwd(rawOutput);
532
+
533
+ // Update CWD tracker
534
+ if (newCwd) {
535
+ cwdTracker.updateCwd(newCwd);
536
+ }
537
+
538
+ const duration = Date.now() - startTime;
539
+
540
+ let text = cleanedOutput;
541
+ if (stderr) {
542
+ text += `\nstderr: ${stderr}`;
543
+ }
544
+ if (timedOut) {
545
+ text += `\nCommand timed out after ${timeout}ms.`;
546
+ }
547
+ if (code !== null && code !== 0) {
548
+ text += `\nExit code: ${code}`;
549
+ }
550
+
551
+ resolve({
552
+ content: [{ type: 'text', text: text || '(no output)' }],
553
+ details: {
554
+ stdout: cleanedOutput,
555
+ stderr,
556
+ exitCode: code,
557
+ duration,
558
+ interrupted: false,
559
+ timedOut,
560
+ backgrounded: false,
561
+ taskId: null,
562
+ finalCwd: newCwd ?? cwdTracker.getCwd(),
563
+ },
564
+ });
565
+ });
566
+
567
+ proc.on('error', (err) => {
568
+ clearTimeout(timeoutTimer);
569
+ clearTimeout(autoYieldTimer);
570
+ if (streamInterval) {
571
+ clearInterval(streamInterval);
572
+ flushStreamUpdate();
573
+ }
574
+
575
+ if (autoYielded) return;
576
+
577
+ resolve({
578
+ content: [{ type: 'text', text: `Failed to execute command: ${err.message}` }],
579
+ details: {
580
+ stdout,
581
+ stderr,
582
+ exitCode: null,
583
+ duration: Date.now() - startTime,
584
+ interrupted: false,
585
+ timedOut: false,
586
+ backgrounded: false,
587
+ taskId: null,
588
+ finalCwd: cwdTracker.getCwd(),
589
+ },
590
+ });
591
+ });
592
+ });
593
+ },
594
+ };
595
+
596
+ return attachRuntimeAwareTool(tool, {
597
+ toolKind: 'Bash',
598
+ cloneForRuntime: (runtime) => createBashTool({
599
+ ...config,
600
+ runtime,
601
+ cwdTracker: runtime.cwdTracker,
602
+ }),
603
+ });
604
+ }
605
+
606
+ // ---------------------------------------------------------------------------
607
+ // Process tree cleanup
608
+ // ---------------------------------------------------------------------------
609
+
610
+ /**
611
+ * Kill the entire process tree.
612
+ * Unix: send SIGKILL to the process group.
613
+ * Windows: use taskkill /F /T.
614
+ */
615
+ function killProcessTree(proc: child_process.ChildProcess): void {
616
+ if (!proc.pid) return;
617
+
618
+ try {
619
+ if (process.platform === 'win32') {
620
+ child_process.execFileSync('taskkill', ['/F', '/T', '/PID', String(proc.pid)], { stdio: 'ignore' });
621
+ } else {
622
+ // Kill the entire process group
623
+ process.kill(-proc.pid, 'SIGKILL');
624
+ }
625
+ } catch {
626
+ // Process may have already exited
627
+ try {
628
+ proc.kill('SIGKILL');
629
+ } catch {
630
+ // Ignore
631
+ }
632
+ }
633
+ }