@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,302 @@
1
+ /**
2
+ * Interactive command detection for Bash.
3
+ *
4
+ * Catches commands that would block waiting for TTY input (editors,
5
+ * pagers, REPLs, interactive DB clients) and rejects them with a
6
+ * concrete non-interactive suggestion. Prevents the agent from burning
7
+ * its entire timeout budget on a hung `vim` or `psql` invocation.
8
+ *
9
+ * This is a UX gate, not a security gate: it sits at the end of the
10
+ * safety cascade after all security layers have passed. Security checks
11
+ * always come first.
12
+ *
13
+ * The module is pure — no I/O, no global state — so the rule set can be
14
+ * exhaustively unit-tested without a shell.
15
+ */
16
+
17
+ import { splitOnShellOperators, type SafetyCheckResult } from './safety.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Rule definition
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface InteractiveRule {
24
+ /** Exact basename of the program to match (e.g. 'vim', not '/usr/bin/vim'). */
25
+ name: string;
26
+ /**
27
+ * Decide whether the invocation is interactive. `args` is the token
28
+ * list AFTER the program (flags and positional args). Return a
29
+ * user-facing suggestion string when the invocation is interactive,
30
+ * or `null` when a non-interactive form has been detected.
31
+ */
32
+ check: (args: string[]) => string | null;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Rule groups
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const EDITORS = ['vim', 'vi', 'nvim', 'emacs', 'emacsclient', 'nano', 'pico', 'ed', 'joe'];
40
+ const MONITORS = ['top', 'htop', 'atop', 'btop', 'watch'];
41
+ const PAGERS = ['less', 'more', 'most'];
42
+
43
+ function alwaysInteractive(name: string, suggestion: string): InteractiveRule {
44
+ return { name, check: () => suggestion };
45
+ }
46
+
47
+ const RULES: InteractiveRule[] = [
48
+ ...EDITORS.map((name) =>
49
+ alwaysInteractive(
50
+ name,
51
+ `${name} is a terminal editor and will block waiting for input. Use the Edit or Write tools to modify files instead.`,
52
+ ),
53
+ ),
54
+ ...MONITORS.map((name) =>
55
+ alwaysInteractive(
56
+ name,
57
+ `${name} runs continuously and blocks the shell. Use a one-shot alternative (e.g. \`ps aux | head\`, \`uptime\`, \`df -h\`).`,
58
+ ),
59
+ ),
60
+ // Pagers block even when piped — they paginate on keypress.
61
+ ...PAGERS.map((name) =>
62
+ alwaysInteractive(
63
+ name,
64
+ `${name} paginates and will block waiting for a keypress. Use \`cat\`, \`head\`, or \`tail\` instead.`,
65
+ ),
66
+ ),
67
+ // Python: interactive unless given a script file or -c/-m.
68
+ ...['python', 'python2', 'python3'].map((name): InteractiveRule => ({
69
+ name,
70
+ check: (args) => pythonCheck(name, args),
71
+ })),
72
+ { name: 'node', check: nodeCheck },
73
+ { name: 'ruby', check: rubyCheck },
74
+ alwaysInteractive(
75
+ 'irb',
76
+ 'irb opens an interactive Ruby shell. Use `ruby -e "..."` for one-off code.',
77
+ ),
78
+ { name: 'mongo', check: (args) => mongoCheck('mongo', args) },
79
+ { name: 'mongosh', check: (args) => mongoCheck('mongosh', args) },
80
+ { name: 'sqlite3', check: sqliteCheck },
81
+ { name: 'psql', check: psqlCheck },
82
+ { name: 'mysql', check: mysqlCheck },
83
+ { name: 'mariadb', check: mysqlCheck },
84
+ ];
85
+
86
+ // Fast lookup by basename.
87
+ const RULES_BY_NAME = new Map(RULES.map((r) => [r.name, r]));
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Public entry point
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Check a full shell command for interactive invocations. Splits on
95
+ * shell operators (`;`, `&&`, `||`, `|`) and inspects each sub-command
96
+ * independently — `cat file | less` is rejected because the `less`
97
+ * sub-command is interactive, even though `cat` is not.
98
+ *
99
+ * Returns the first interactive sub-command's rejection; if all
100
+ * sub-commands are non-interactive, returns `{ allowed: true }`.
101
+ */
102
+ export function checkInteractive(command: string): SafetyCheckResult {
103
+ const subs = splitOnShellOperators(command);
104
+ for (const sub of subs) {
105
+ const tokens = tokenize(sub);
106
+ const program = findProgram(tokens);
107
+ if (!program) continue;
108
+ const rule = RULES_BY_NAME.get(program.name);
109
+ if (!rule) continue;
110
+ const suggestion = rule.check(program.args);
111
+ if (suggestion !== null) {
112
+ return {
113
+ allowed: false,
114
+ reason: `Interactive command detected. ${suggestion}`,
115
+ };
116
+ }
117
+ }
118
+ return { allowed: true };
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Tokenization
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Minimal shell-aware tokenizer: splits on unquoted whitespace, keeps
127
+ * single/double-quoted regions intact (quotes themselves are stripped),
128
+ * and honors backslash escapes. Sufficient for identifying the program
129
+ * token and distinguishing flags from positional args; not a complete
130
+ * POSIX shell parser.
131
+ */
132
+ export function tokenize(command: string): string[] {
133
+ const tokens: string[] = [];
134
+ let current = '';
135
+ let inSingle = false;
136
+ let inDouble = false;
137
+ for (let i = 0; i < command.length; i++) {
138
+ const ch = command[i]!;
139
+ if (!inSingle && ch === '\\' && i + 1 < command.length) {
140
+ current += command[i + 1]!;
141
+ i++;
142
+ continue;
143
+ }
144
+ if (!inDouble && ch === "'") {
145
+ inSingle = !inSingle;
146
+ continue;
147
+ }
148
+ if (!inSingle && ch === '"') {
149
+ inDouble = !inDouble;
150
+ continue;
151
+ }
152
+ if (!inSingle && !inDouble && /\s/u.test(ch)) {
153
+ if (current.length > 0) {
154
+ tokens.push(current);
155
+ current = '';
156
+ }
157
+ continue;
158
+ }
159
+ current += ch;
160
+ }
161
+ if (current.length > 0) tokens.push(current);
162
+ return tokens;
163
+ }
164
+
165
+ /**
166
+ * Find the effective program name and argument list, accounting for:
167
+ * 1. Leading `KEY=VALUE` env var prefixes (`FOO=bar vim file`)
168
+ * 2. The `env` wrapper (`env FOO=bar vim file` or `env -u BAR vim`)
169
+ *
170
+ * Returns the program's basename (last path segment) and the remaining
171
+ * arg tokens, or `null` if no program token is present.
172
+ */
173
+ export function findProgram(
174
+ tokens: string[],
175
+ ): { name: string; args: string[] } | null {
176
+ let i = 0;
177
+ while (i < tokens.length && isEnvAssignment(tokens[i]!)) i++;
178
+
179
+ if (i < tokens.length && tokens[i] === 'env') {
180
+ i++;
181
+ // `env`'s flags that take a subsequent argument. We need to skip
182
+ // both the flag and its value so a value that doesn't look like a
183
+ // flag (e.g. `env -u OLD vim`) isn't mistaken for the program.
184
+ const ARG_FLAGS_SHORT = new Set(['-u', '-C', '-S']);
185
+ const ARG_FLAGS_LONG = new Set(['--unset', '--chdir', '--split-string']);
186
+ while (i < tokens.length) {
187
+ const t = tokens[i]!;
188
+ if (isEnvAssignment(t)) {
189
+ i++;
190
+ continue;
191
+ }
192
+ if (!t.startsWith('-')) break;
193
+ if (ARG_FLAGS_SHORT.has(t) || ARG_FLAGS_LONG.has(t)) {
194
+ i += 2; // consume flag + value
195
+ continue;
196
+ }
197
+ // --long=value form or flags with no arg (-i, -0, --verbose, etc.)
198
+ i++;
199
+ }
200
+ }
201
+
202
+ if (i >= tokens.length) return null;
203
+ const prog = tokens[i]!;
204
+ const basename = prog.split('/').pop() ?? prog;
205
+ return { name: basename, args: tokens.slice(i + 1) };
206
+ }
207
+
208
+ function isEnvAssignment(token: string): boolean {
209
+ return /^[A-Za-z_][A-Za-z0-9_]*=/u.test(token);
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Per-program predicates
214
+ // ---------------------------------------------------------------------------
215
+
216
+ function pythonCheck(name: string, args: string[]): string | null {
217
+ for (let i = 0; i < args.length; i++) {
218
+ const arg = args[i]!;
219
+ // -c / -m consume the next arg but the presence alone is enough.
220
+ if (arg === '-c' || arg === '-m') return null;
221
+ if (arg === '-V' || arg === '--version') return null;
222
+ if (arg === '-h' || arg === '--help') return null;
223
+ // Script file.
224
+ if (/\.py[ocw]?$/u.test(arg) && !arg.startsWith('-')) return null;
225
+ // Any positional arg is treated as a script (matches how Python's argv
226
+ // parser works).
227
+ if (!arg.startsWith('-')) return null;
228
+ }
229
+ return `${name} without a script or \`-c\`/\`-m\` starts an interactive REPL and will block. Provide a script file or use \`${name} -c "..."\`.`;
230
+ }
231
+
232
+ function nodeCheck(args: string[]): string | null {
233
+ for (const arg of args) {
234
+ if (
235
+ arg === '-e' ||
236
+ arg === '--eval' ||
237
+ arg === '-p' ||
238
+ arg === '--print' ||
239
+ arg === '-v' ||
240
+ arg === '--version' ||
241
+ arg === '-h' ||
242
+ arg === '--help'
243
+ ) {
244
+ return null;
245
+ }
246
+ if (!arg.startsWith('-')) return null; // script file or --file
247
+ }
248
+ return 'node without a script or `-e` starts an interactive REPL and will block. Provide a script file or use `node -e "..."`.';
249
+ }
250
+
251
+ function rubyCheck(args: string[]): string | null {
252
+ for (const arg of args) {
253
+ if (arg === '-e' || arg === '-v' || arg === '--version' || arg === '-h' || arg === '--help') {
254
+ return null;
255
+ }
256
+ if (!arg.startsWith('-')) return null;
257
+ }
258
+ return 'ruby without a script or `-e` reads from stdin and will block. Provide a `.rb` file or use `ruby -e "..."`.';
259
+ }
260
+
261
+ function mongoCheck(name: string, args: string[]): string | null {
262
+ if (args.includes('--eval') || args.includes('-e') || args.includes('--version') || args.includes('--help')) {
263
+ return null;
264
+ }
265
+ // Executing a script file is also non-interactive.
266
+ if (args.some((a) => /\.js$/u.test(a) && !a.startsWith('-'))) return null;
267
+ return `${name} without \`--eval\` or a script file opens an interactive shell. Use \`${name} --eval "..."\`.`;
268
+ }
269
+
270
+ function sqliteCheck(args: string[]): string | null {
271
+ if (args.includes('-cmd') || args.includes('-batch') || args.includes('-version') || args.includes('--help')) {
272
+ return null;
273
+ }
274
+ // sqlite3 <db> "<sql>" — two or more non-flag args means the second is a query.
275
+ const nonFlag = args.filter((a) => !a.startsWith('-'));
276
+ if (nonFlag.length >= 2) return null;
277
+ return 'sqlite3 without a SQL argument opens an interactive prompt. Use `sqlite3 <db> "<sql>"` or pass `-cmd "..."`.';
278
+ }
279
+
280
+ function psqlCheck(args: string[]): string | null {
281
+ // -c / --command: inline SQL. -f / --file: script file. -l / --list: list DBs.
282
+ // --version / -V: version. All are single-shot, non-interactive.
283
+ // NOTE: -h means "host" in psql, NOT help. We do not treat it as safe.
284
+ const safeFlags = new Set([
285
+ '-c', '--command', '-f', '--file', '-l', '--list', '--version', '-V', '--help',
286
+ ]);
287
+ for (const arg of args) {
288
+ if (safeFlags.has(arg)) return null;
289
+ // Flags written as --command=VALUE.
290
+ if (arg.startsWith('--command=') || arg.startsWith('--file=')) return null;
291
+ }
292
+ return 'psql without `-c` or `-f` opens an interactive prompt. Use `psql -c "SELECT ..."` or `psql -f script.sql`.';
293
+ }
294
+
295
+ function mysqlCheck(args: string[]): string | null {
296
+ const safeFlags = new Set(['-e', '--execute', '--version', '-V', '--help', '-?']);
297
+ for (const arg of args) {
298
+ if (safeFlags.has(arg)) return null;
299
+ if (arg.startsWith('--execute=')) return null;
300
+ }
301
+ return 'mysql without `-e` opens an interactive prompt. Use `mysql -e "SELECT ..."`.';
302
+ }