centaurus-cli 3.0.1 → 3.1.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 (552) hide show
  1. package/dist/ai/types.js +0 -1
  2. package/dist/ai/types.js.map +1 -1
  3. package/dist/cli-adapter.js +5047 -5158
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/commands/CommandParser.js +372 -315
  6. package/dist/commands/CommandParser.js.map +1 -1
  7. package/dist/config/build-config.js +11 -42
  8. package/dist/config/build-config.js.map +1 -1
  9. package/dist/config/defaultConfig.js +94 -82
  10. package/dist/config/defaultConfig.js.map +1 -1
  11. package/dist/config/manager.js +144 -160
  12. package/dist/config/manager.js.map +1 -1
  13. package/dist/config/mcp-config-manager.js +411 -364
  14. package/dist/config/mcp-config-manager.js.map +1 -1
  15. package/dist/config/models.js +118 -185
  16. package/dist/config/models.js.map +1 -1
  17. package/dist/config/slash-commands.js +186 -184
  18. package/dist/config/slash-commands.js.map +1 -1
  19. package/dist/config/types.js +33 -26
  20. package/dist/config/types.js.map +1 -1
  21. package/dist/context/command-detector.js +63 -67
  22. package/dist/context/command-detector.js.map +1 -1
  23. package/dist/context/context-manager.js +533 -518
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/handlers/docker-handler.js +518 -576
  26. package/dist/context/handlers/docker-handler.js.map +1 -1
  27. package/dist/context/handlers/ssh-handler.js +1050 -1109
  28. package/dist/context/handlers/ssh-handler.js.map +1 -1
  29. package/dist/context/handlers/wsl-handler.js +558 -630
  30. package/dist/context/handlers/wsl-handler.js.map +1 -1
  31. package/dist/context/index.js +42 -6
  32. package/dist/context/index.js.map +1 -1
  33. package/dist/context/subshell-handler.js +0 -4
  34. package/dist/context/subshell-handler.js.map +1 -1
  35. package/dist/context/types.js +20 -31
  36. package/dist/context/types.js.map +1 -1
  37. package/dist/hooks/useConnectivity.js +13 -10
  38. package/dist/hooks/useConnectivity.js.map +1 -1
  39. package/dist/hooks/useTerminalDimensions.js +67 -79
  40. package/dist/hooks/useTerminalDimensions.js.map +1 -1
  41. package/dist/index.js +228 -251
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcp/mcp-command-handler.js +297 -260
  44. package/dist/mcp/mcp-command-handler.js.map +1 -1
  45. package/dist/mcp/mcp-server-manager.js +139 -155
  46. package/dist/mcp/mcp-server-manager.js.map +1 -1
  47. package/dist/mcp/mcp-tool-wrapper.js +74 -94
  48. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  49. package/dist/services/ai-autocomplete-agent.js +169 -181
  50. package/dist/services/ai-autocomplete-agent.js.map +1 -1
  51. package/dist/services/ai-context-injector.js +180 -93
  52. package/dist/services/ai-context-injector.js.map +1 -1
  53. package/dist/services/ai-service-client.js +513 -456
  54. package/dist/services/ai-service-client.js.map +1 -1
  55. package/dist/services/api-client.js +443 -441
  56. package/dist/services/api-client.js.map +1 -1
  57. package/dist/services/auth-handler.js +162 -198
  58. package/dist/services/auth-handler.js.map +1 -1
  59. package/dist/services/background-task-manager.js +258 -282
  60. package/dist/services/background-task-manager.js.map +1 -1
  61. package/dist/services/checkpoint-manager.js +1526 -1512
  62. package/dist/services/checkpoint-manager.js.map +1 -1
  63. package/dist/services/clipboard-service.js +151 -200
  64. package/dist/services/clipboard-service.js.map +1 -1
  65. package/dist/services/connectivity-manager.js +63 -65
  66. package/dist/services/connectivity-manager.js.map +1 -1
  67. package/dist/services/conversation-manager.js +118 -121
  68. package/dist/services/conversation-manager.js.map +1 -1
  69. package/dist/services/environment-context-injector.js +160 -187
  70. package/dist/services/environment-context-injector.js.map +1 -1
  71. package/dist/services/fast-context-agent.js +203 -243
  72. package/dist/services/fast-context-agent.js.map +1 -1
  73. package/dist/services/input-detection-agent.js +190 -202
  74. package/dist/services/input-detection-agent.js.map +1 -1
  75. package/dist/services/input-requirement-detector.js +155 -189
  76. package/dist/services/input-requirement-detector.js.map +1 -1
  77. package/dist/services/local-chat-storage.js +342 -365
  78. package/dist/services/local-chat-storage.js.map +1 -1
  79. package/dist/services/monitored-shell-manager.js +225 -233
  80. package/dist/services/monitored-shell-manager.js.map +1 -1
  81. package/dist/services/ollama-service.js +293 -310
  82. package/dist/services/ollama-service.js.map +1 -1
  83. package/dist/services/rules-storage.js +142 -0
  84. package/dist/services/rules-storage.js.map +1 -0
  85. package/dist/services/session-quota-manager.js +219 -235
  86. package/dist/services/session-quota-manager.js.map +1 -1
  87. package/dist/services/shell-input-agent.js +299 -334
  88. package/dist/services/shell-input-agent.js.map +1 -1
  89. package/dist/services/sub-agent-manager.js +459 -501
  90. package/dist/services/sub-agent-manager.js.map +1 -1
  91. package/dist/services/warpify-detector.js +133 -183
  92. package/dist/services/warpify-detector.js.map +1 -1
  93. package/dist/services/workflow-storage.js +202 -217
  94. package/dist/services/workflow-storage.js.map +1 -1
  95. package/dist/test-ssh-handler.js +148 -193
  96. package/dist/test-ssh-handler.js.map +1 -1
  97. package/dist/tools/add-mcp.js +161 -0
  98. package/dist/tools/add-mcp.js.map +1 -0
  99. package/dist/tools/background-command.js +240 -273
  100. package/dist/tools/background-command.js.map +1 -1
  101. package/dist/tools/command.js +447 -440
  102. package/dist/tools/command.js.map +1 -1
  103. package/dist/tools/create-image.js +172 -202
  104. package/dist/tools/create-image.js.map +1 -1
  105. package/dist/tools/enter-remote-session.js +169 -215
  106. package/dist/tools/enter-remote-session.js.map +1 -1
  107. package/dist/tools/fast-context.js +60 -67
  108. package/dist/tools/fast-context.js.map +1 -1
  109. package/dist/tools/file-ops.js +601 -572
  110. package/dist/tools/file-ops.js.map +1 -1
  111. package/dist/tools/find-files.js +262 -303
  112. package/dist/tools/find-files.js.map +1 -1
  113. package/dist/tools/get-diff.js +423 -406
  114. package/dist/tools/get-diff.js.map +1 -1
  115. package/dist/tools/grep-search.js +966 -948
  116. package/dist/tools/grep-search.js.map +1 -1
  117. package/dist/tools/inspect-symbol.js +308 -323
  118. package/dist/tools/inspect-symbol.js.map +1 -1
  119. package/dist/tools/plan-mode.js +459 -503
  120. package/dist/tools/plan-mode.js.map +1 -1
  121. package/dist/tools/read-binary-file.js +160 -190
  122. package/dist/tools/read-binary-file.js.map +1 -1
  123. package/dist/tools/registry.js +100 -84
  124. package/dist/tools/registry.js.map +1 -1
  125. package/dist/tools/reproduce_issue.js +170 -151
  126. package/dist/tools/reproduce_issue.js.map +1 -1
  127. package/dist/tools/sub-agent.js +223 -228
  128. package/dist/tools/sub-agent.js.map +1 -1
  129. package/dist/tools/task-complete.js +28 -27
  130. package/dist/tools/task-complete.js.map +1 -1
  131. package/dist/tools/types.js +0 -1
  132. package/dist/tools/types.js.map +1 -1
  133. package/dist/tools/validation.js +96 -118
  134. package/dist/tools/validation.js.map +1 -1
  135. package/dist/tools/web-search.js +194 -194
  136. package/dist/tools/web-search.js.map +1 -1
  137. package/dist/tools/workflow-tool.js +77 -82
  138. package/dist/tools/workflow-tool.js.map +1 -1
  139. package/dist/types/index.js +0 -1
  140. package/dist/types/index.js.map +1 -1
  141. package/dist/types/rule.js +1 -0
  142. package/dist/types/rule.js.map +1 -0
  143. package/dist/types/workflow.js +0 -7
  144. package/dist/types/workflow.js.map +1 -1
  145. package/dist/ui/components/AgentTimer.js +24 -25
  146. package/dist/ui/components/AgentTimer.js.map +1 -1
  147. package/dist/ui/components/App.js +3266 -3263
  148. package/dist/ui/components/App.js.map +1 -1
  149. package/dist/ui/components/AuthScreen.js +22 -34
  150. package/dist/ui/components/AuthScreen.js.map +1 -1
  151. package/dist/ui/components/AuthWelcomeScreen.js +30 -24
  152. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  153. package/dist/ui/components/Breadcrumbs.js +53 -82
  154. package/dist/ui/components/Breadcrumbs.js.map +1 -1
  155. package/dist/ui/components/CircularSelectInput.js +59 -67
  156. package/dist/ui/components/CircularSelectInput.js.map +1 -1
  157. package/dist/ui/components/ClipboardFileAutocomplete.js +78 -39
  158. package/dist/ui/components/ClipboardFileAutocomplete.js.map +1 -1
  159. package/dist/ui/components/CodeBlock.js +24 -42
  160. package/dist/ui/components/CodeBlock.js.map +1 -1
  161. package/dist/ui/components/ConfigViewer.js +18 -25
  162. package/dist/ui/components/ConfigViewer.js.map +1 -1
  163. package/dist/ui/components/ConfirmPrompt.js +49 -71
  164. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  165. package/dist/ui/components/ConnectionStatusMessage.js +32 -83
  166. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  167. package/dist/ui/components/ContextWindowIndicator.js +34 -49
  168. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  169. package/dist/ui/components/DetailedPlanReviewScreen.js +104 -106
  170. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  171. package/dist/ui/components/DiffViewer.js +68 -121
  172. package/dist/ui/components/DiffViewer.js.map +1 -1
  173. package/dist/ui/components/ErrorBoundary.js +40 -48
  174. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  175. package/dist/ui/components/FileCreationPreview.js +29 -60
  176. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  177. package/dist/ui/components/FileOperation.js +34 -29
  178. package/dist/ui/components/FileOperation.js.map +1 -1
  179. package/dist/ui/components/FileTagAutocomplete.js +55 -25
  180. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  181. package/dist/ui/components/FontRecommendation.js.map +1 -1
  182. package/dist/ui/components/GitDiffBreadcrumb.js +29 -0
  183. package/dist/ui/components/GitDiffBreadcrumb.js.map +1 -0
  184. package/dist/ui/components/InputBox.js +1620 -2150
  185. package/dist/ui/components/InputBox.js.map +1 -1
  186. package/dist/ui/components/InteractiveShell.js +234 -352
  187. package/dist/ui/components/InteractiveShell.js.map +1 -1
  188. package/dist/ui/components/KeyboardHelp.js +34 -35
  189. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  190. package/dist/ui/components/LoadingIndicator.js +22 -25
  191. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  192. package/dist/ui/components/MCPAddScreen.js +40 -51
  193. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  194. package/dist/ui/components/MCPListScreen.js +40 -48
  195. package/dist/ui/components/MCPListScreen.js.map +1 -1
  196. package/dist/ui/components/MCPServerListScreen.js +49 -56
  197. package/dist/ui/components/MCPServerListScreen.js.map +1 -1
  198. package/dist/ui/components/MarkdownRenderer.js +69 -96
  199. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  200. package/dist/ui/components/MessageBox.js +66 -48
  201. package/dist/ui/components/MessageBox.js.map +1 -1
  202. package/dist/ui/components/MessageDisplay.js +150 -142
  203. package/dist/ui/components/MessageDisplay.js.map +1 -1
  204. package/dist/ui/components/MonitorModeAIPanel.js +46 -65
  205. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  206. package/dist/ui/components/MultiLineInput.js +243 -277
  207. package/dist/ui/components/MultiLineInput.js.map +1 -1
  208. package/dist/ui/components/PasswordPrompt.js +37 -18
  209. package/dist/ui/components/PasswordPrompt.js.map +1 -1
  210. package/dist/ui/components/PlanAcceptedMessage.js +27 -38
  211. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  212. package/dist/ui/components/PlanReviewScreen.js +46 -50
  213. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  214. package/dist/ui/components/RulesEditorScreen.js +81 -0
  215. package/dist/ui/components/RulesEditorScreen.js.map +1 -0
  216. package/dist/ui/components/SelectPrompt.js +19 -8
  217. package/dist/ui/components/SelectPrompt.js.map +1 -1
  218. package/dist/ui/components/ShimmerText.js +44 -0
  219. package/dist/ui/components/ShimmerText.js.map +1 -0
  220. package/dist/ui/components/SlashCommandAutocomplete.js +49 -22
  221. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  222. package/dist/ui/components/StatusBar.js +56 -87
  223. package/dist/ui/components/StatusBar.js.map +1 -1
  224. package/dist/ui/components/StreamingMessageDisplay.js +116 -99
  225. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  226. package/dist/ui/components/TaskCompletedMessage.js +28 -23
  227. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  228. package/dist/ui/components/TaskProgressIndicator.js +44 -70
  229. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  230. package/dist/ui/components/ThinkingDisplay.js +44 -41
  231. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  232. package/dist/ui/components/ToolExecutionMessage.js +772 -1326
  233. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  234. package/dist/ui/components/ToolExecutionStatus.js +53 -84
  235. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  236. package/dist/ui/components/ToolResult.js +22 -15
  237. package/dist/ui/components/ToolResult.js.map +1 -1
  238. package/dist/ui/components/VersionUpdatePrompt.js +88 -120
  239. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  240. package/dist/ui/components/WelcomeBanner.js +176 -26
  241. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  242. package/dist/ui/components/WorkflowCreatorScreen.js +94 -161
  243. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  244. package/dist/utils/ansi-encoder.js +30 -61
  245. package/dist/utils/ansi-encoder.js.map +1 -1
  246. package/dist/utils/chat-formatter.js +327 -305
  247. package/dist/utils/chat-formatter.js.map +1 -1
  248. package/dist/utils/command-history.js +152 -174
  249. package/dist/utils/command-history.js.map +1 -1
  250. package/dist/utils/context-sanitizer.js +49 -112
  251. package/dist/utils/context-sanitizer.js.map +1 -1
  252. package/dist/utils/conversation-logger.js +292 -324
  253. package/dist/utils/conversation-logger.js.map +1 -1
  254. package/dist/utils/custom-commands-manager.js +126 -131
  255. package/dist/utils/custom-commands-manager.js.map +1 -1
  256. package/dist/utils/editor-utils.js +732 -837
  257. package/dist/utils/editor-utils.js.map +1 -1
  258. package/dist/utils/file.js +174 -213
  259. package/dist/utils/file.js.map +1 -1
  260. package/dist/utils/git-stats.js +169 -0
  261. package/dist/utils/git-stats.js.map +1 -0
  262. package/dist/utils/input-classifier.js +960 -482
  263. package/dist/utils/input-classifier.js.map +1 -1
  264. package/dist/utils/logger.js +48 -73
  265. package/dist/utils/logger.js.map +1 -1
  266. package/dist/utils/markdown-parser.js +277 -310
  267. package/dist/utils/markdown-parser.js.map +1 -1
  268. package/dist/utils/rule-reference-resolver.js +54 -0
  269. package/dist/utils/rule-reference-resolver.js.map +1 -0
  270. package/dist/utils/shell.js +144 -156
  271. package/dist/utils/shell.js.map +1 -1
  272. package/dist/utils/state.js +23 -22
  273. package/dist/utils/state.js.map +1 -1
  274. package/dist/utils/syntax-checker.js +279 -327
  275. package/dist/utils/syntax-checker.js.map +1 -1
  276. package/dist/utils/terminal-output.js +199 -302
  277. package/dist/utils/terminal-output.js.map +1 -1
  278. package/dist/utils/text-clipboard.js +47 -70
  279. package/dist/utils/text-clipboard.js.map +1 -1
  280. package/dist/utils/unicode-sanitizer.js +134 -197
  281. package/dist/utils/unicode-sanitizer.js.map +1 -1
  282. package/dist/utils/version-checker.js +46 -56
  283. package/dist/utils/version-checker.js.map +1 -1
  284. package/package.json +6 -4
  285. package/dist/ai/types.d.ts +0 -20
  286. package/dist/ai/types.d.ts.map +0 -1
  287. package/dist/cli-adapter.d.ts +0 -514
  288. package/dist/cli-adapter.d.ts.map +0 -1
  289. package/dist/commands/CommandParser.d.ts +0 -27
  290. package/dist/commands/CommandParser.d.ts.map +0 -1
  291. package/dist/config/build-config.d.ts +0 -42
  292. package/dist/config/build-config.d.ts.map +0 -1
  293. package/dist/config/defaultConfig.d.ts +0 -79
  294. package/dist/config/defaultConfig.d.ts.map +0 -1
  295. package/dist/config/manager.d.ts +0 -62
  296. package/dist/config/manager.d.ts.map +0 -1
  297. package/dist/config/mcp-config-manager.d.ts +0 -79
  298. package/dist/config/mcp-config-manager.d.ts.map +0 -1
  299. package/dist/config/models.d.ts +0 -83
  300. package/dist/config/models.d.ts.map +0 -1
  301. package/dist/config/slash-commands.d.ts +0 -23
  302. package/dist/config/slash-commands.d.ts.map +0 -1
  303. package/dist/config/types.d.ts +0 -35
  304. package/dist/config/types.d.ts.map +0 -1
  305. package/dist/context/command-detector.d.ts +0 -50
  306. package/dist/context/command-detector.d.ts.map +0 -1
  307. package/dist/context/context-manager.d.ts +0 -157
  308. package/dist/context/context-manager.d.ts.map +0 -1
  309. package/dist/context/handlers/docker-handler.d.ts +0 -130
  310. package/dist/context/handlers/docker-handler.d.ts.map +0 -1
  311. package/dist/context/handlers/ssh-handler.d.ts +0 -201
  312. package/dist/context/handlers/ssh-handler.d.ts.map +0 -1
  313. package/dist/context/handlers/wsl-handler.d.ts +0 -146
  314. package/dist/context/handlers/wsl-handler.d.ts.map +0 -1
  315. package/dist/context/index.d.ts +0 -8
  316. package/dist/context/index.d.ts.map +0 -1
  317. package/dist/context/subshell-handler.d.ts +0 -165
  318. package/dist/context/subshell-handler.d.ts.map +0 -1
  319. package/dist/context/types.d.ts +0 -70
  320. package/dist/context/types.d.ts.map +0 -1
  321. package/dist/hooks/useConnectivity.d.ts +0 -2
  322. package/dist/hooks/useConnectivity.d.ts.map +0 -1
  323. package/dist/hooks/useTerminalDimensions.d.ts +0 -41
  324. package/dist/hooks/useTerminalDimensions.d.ts.map +0 -1
  325. package/dist/index.d.ts +0 -9
  326. package/dist/index.d.ts.map +0 -1
  327. package/dist/mcp/mcp-command-handler.d.ts +0 -47
  328. package/dist/mcp/mcp-command-handler.d.ts.map +0 -1
  329. package/dist/mcp/mcp-server-manager.d.ts +0 -30
  330. package/dist/mcp/mcp-server-manager.d.ts.map +0 -1
  331. package/dist/mcp/mcp-tool-wrapper.d.ts +0 -12
  332. package/dist/mcp/mcp-tool-wrapper.d.ts.map +0 -1
  333. package/dist/services/ai-autocomplete-agent.d.ts +0 -39
  334. package/dist/services/ai-autocomplete-agent.d.ts.map +0 -1
  335. package/dist/services/ai-context-injector.d.ts +0 -41
  336. package/dist/services/ai-context-injector.d.ts.map +0 -1
  337. package/dist/services/ai-service-client.d.ts +0 -128
  338. package/dist/services/ai-service-client.d.ts.map +0 -1
  339. package/dist/services/api-client.d.ts +0 -353
  340. package/dist/services/api-client.d.ts.map +0 -1
  341. package/dist/services/auth-handler.d.ts +0 -30
  342. package/dist/services/auth-handler.d.ts.map +0 -1
  343. package/dist/services/background-task-manager.d.ts +0 -114
  344. package/dist/services/background-task-manager.d.ts.map +0 -1
  345. package/dist/services/checkpoint-manager.d.ts +0 -204
  346. package/dist/services/checkpoint-manager.d.ts.map +0 -1
  347. package/dist/services/clipboard-service.d.ts +0 -37
  348. package/dist/services/clipboard-service.d.ts.map +0 -1
  349. package/dist/services/connectivity-manager.d.ts +0 -18
  350. package/dist/services/connectivity-manager.d.ts.map +0 -1
  351. package/dist/services/conversation-manager.d.ts +0 -73
  352. package/dist/services/conversation-manager.d.ts.map +0 -1
  353. package/dist/services/environment-context-injector.d.ts +0 -69
  354. package/dist/services/environment-context-injector.d.ts.map +0 -1
  355. package/dist/services/fast-context-agent.d.ts +0 -12
  356. package/dist/services/fast-context-agent.d.ts.map +0 -1
  357. package/dist/services/input-detection-agent.d.ts +0 -40
  358. package/dist/services/input-detection-agent.d.ts.map +0 -1
  359. package/dist/services/input-requirement-detector.d.ts +0 -28
  360. package/dist/services/input-requirement-detector.d.ts.map +0 -1
  361. package/dist/services/local-chat-storage.d.ts +0 -182
  362. package/dist/services/local-chat-storage.d.ts.map +0 -1
  363. package/dist/services/monitored-shell-manager.d.ts +0 -120
  364. package/dist/services/monitored-shell-manager.d.ts.map +0 -1
  365. package/dist/services/ollama-service.d.ts +0 -197
  366. package/dist/services/ollama-service.d.ts.map +0 -1
  367. package/dist/services/session-quota-manager.d.ts +0 -101
  368. package/dist/services/session-quota-manager.d.ts.map +0 -1
  369. package/dist/services/shell-input-agent.d.ts +0 -89
  370. package/dist/services/shell-input-agent.d.ts.map +0 -1
  371. package/dist/services/sub-agent-manager.d.ts +0 -140
  372. package/dist/services/sub-agent-manager.d.ts.map +0 -1
  373. package/dist/services/warpify-detector.d.ts +0 -43
  374. package/dist/services/warpify-detector.d.ts.map +0 -1
  375. package/dist/services/workflow-storage.d.ts +0 -72
  376. package/dist/services/workflow-storage.d.ts.map +0 -1
  377. package/dist/test-ssh-handler.d.ts +0 -8
  378. package/dist/test-ssh-handler.d.ts.map +0 -1
  379. package/dist/tools/background-command.d.ts +0 -11
  380. package/dist/tools/background-command.d.ts.map +0 -1
  381. package/dist/tools/command.d.ts +0 -3
  382. package/dist/tools/command.d.ts.map +0 -1
  383. package/dist/tools/create-image.d.ts +0 -10
  384. package/dist/tools/create-image.d.ts.map +0 -1
  385. package/dist/tools/enter-remote-session.d.ts +0 -48
  386. package/dist/tools/enter-remote-session.d.ts.map +0 -1
  387. package/dist/tools/fast-context.d.ts +0 -3
  388. package/dist/tools/fast-context.d.ts.map +0 -1
  389. package/dist/tools/file-ops.d.ts +0 -7
  390. package/dist/tools/file-ops.d.ts.map +0 -1
  391. package/dist/tools/find-files.d.ts +0 -49
  392. package/dist/tools/find-files.d.ts.map +0 -1
  393. package/dist/tools/get-diff.d.ts +0 -14
  394. package/dist/tools/get-diff.d.ts.map +0 -1
  395. package/dist/tools/grep-search.d.ts +0 -155
  396. package/dist/tools/grep-search.d.ts.map +0 -1
  397. package/dist/tools/inspect-symbol.d.ts +0 -32
  398. package/dist/tools/inspect-symbol.d.ts.map +0 -1
  399. package/dist/tools/plan-mode.d.ts +0 -140
  400. package/dist/tools/plan-mode.d.ts.map +0 -1
  401. package/dist/tools/read-binary-file.d.ts +0 -10
  402. package/dist/tools/read-binary-file.d.ts.map +0 -1
  403. package/dist/tools/registry.d.ts +0 -31
  404. package/dist/tools/registry.d.ts.map +0 -1
  405. package/dist/tools/reproduce_issue.d.ts +0 -2
  406. package/dist/tools/reproduce_issue.d.ts.map +0 -1
  407. package/dist/tools/sub-agent.d.ts +0 -9
  408. package/dist/tools/sub-agent.d.ts.map +0 -1
  409. package/dist/tools/task-complete.d.ts +0 -3
  410. package/dist/tools/task-complete.d.ts.map +0 -1
  411. package/dist/tools/types.d.ts +0 -40
  412. package/dist/tools/types.d.ts.map +0 -1
  413. package/dist/tools/validation.d.ts +0 -47
  414. package/dist/tools/validation.d.ts.map +0 -1
  415. package/dist/tools/web-search.d.ts +0 -24
  416. package/dist/tools/web-search.d.ts.map +0 -1
  417. package/dist/tools/workflow-tool.d.ts +0 -11
  418. package/dist/tools/workflow-tool.d.ts.map +0 -1
  419. package/dist/types/index.d.ts +0 -123
  420. package/dist/types/index.d.ts.map +0 -1
  421. package/dist/types/workflow.d.ts +0 -110
  422. package/dist/types/workflow.d.ts.map +0 -1
  423. package/dist/ui/components/AgentTimer.d.ts +0 -7
  424. package/dist/ui/components/AgentTimer.d.ts.map +0 -1
  425. package/dist/ui/components/App.d.ts +0 -197
  426. package/dist/ui/components/App.d.ts.map +0 -1
  427. package/dist/ui/components/AuthScreen.d.ts +0 -8
  428. package/dist/ui/components/AuthScreen.d.ts.map +0 -1
  429. package/dist/ui/components/AuthWelcomeScreen.d.ts +0 -8
  430. package/dist/ui/components/AuthWelcomeScreen.d.ts.map +0 -1
  431. package/dist/ui/components/Breadcrumbs.d.ts +0 -13
  432. package/dist/ui/components/Breadcrumbs.d.ts.map +0 -1
  433. package/dist/ui/components/CircularSelectInput.d.ts +0 -24
  434. package/dist/ui/components/CircularSelectInput.d.ts.map +0 -1
  435. package/dist/ui/components/ClipboardFileAutocomplete.d.ts +0 -10
  436. package/dist/ui/components/ClipboardFileAutocomplete.d.ts.map +0 -1
  437. package/dist/ui/components/CodeBlock.d.ts +0 -9
  438. package/dist/ui/components/CodeBlock.d.ts.map +0 -1
  439. package/dist/ui/components/ConfigViewer.d.ts +0 -11
  440. package/dist/ui/components/ConfigViewer.d.ts.map +0 -1
  441. package/dist/ui/components/ConfirmPrompt.d.ts +0 -13
  442. package/dist/ui/components/ConfirmPrompt.d.ts.map +0 -1
  443. package/dist/ui/components/ConnectionStatusMessage.d.ts +0 -17
  444. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +0 -1
  445. package/dist/ui/components/ContextWindowIndicator.d.ts +0 -8
  446. package/dist/ui/components/ContextWindowIndicator.d.ts.map +0 -1
  447. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +0 -17
  448. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +0 -1
  449. package/dist/ui/components/DiffViewer.d.ts +0 -9
  450. package/dist/ui/components/DiffViewer.d.ts.map +0 -1
  451. package/dist/ui/components/ErrorBoundary.d.ts +0 -17
  452. package/dist/ui/components/ErrorBoundary.d.ts.map +0 -1
  453. package/dist/ui/components/FileCreationPreview.d.ts +0 -8
  454. package/dist/ui/components/FileCreationPreview.d.ts.map +0 -1
  455. package/dist/ui/components/FileOperation.d.ts +0 -10
  456. package/dist/ui/components/FileOperation.d.ts.map +0 -1
  457. package/dist/ui/components/FileTagAutocomplete.d.ts +0 -11
  458. package/dist/ui/components/FileTagAutocomplete.d.ts.map +0 -1
  459. package/dist/ui/components/FontRecommendation.d.ts +0 -1
  460. package/dist/ui/components/FontRecommendation.d.ts.map +0 -1
  461. package/dist/ui/components/InputBox.d.ts +0 -42
  462. package/dist/ui/components/InputBox.d.ts.map +0 -1
  463. package/dist/ui/components/InteractiveShell.d.ts +0 -30
  464. package/dist/ui/components/InteractiveShell.d.ts.map +0 -1
  465. package/dist/ui/components/KeyboardHelp.d.ts +0 -7
  466. package/dist/ui/components/KeyboardHelp.d.ts.map +0 -1
  467. package/dist/ui/components/LoadingIndicator.d.ts +0 -3
  468. package/dist/ui/components/LoadingIndicator.d.ts.map +0 -1
  469. package/dist/ui/components/MCPAddScreen.d.ts +0 -13
  470. package/dist/ui/components/MCPAddScreen.d.ts.map +0 -1
  471. package/dist/ui/components/MCPListScreen.d.ts +0 -17
  472. package/dist/ui/components/MCPListScreen.d.ts.map +0 -1
  473. package/dist/ui/components/MCPServerListScreen.d.ts +0 -16
  474. package/dist/ui/components/MCPServerListScreen.d.ts.map +0 -1
  475. package/dist/ui/components/MarkdownRenderer.d.ts +0 -8
  476. package/dist/ui/components/MarkdownRenderer.d.ts.map +0 -1
  477. package/dist/ui/components/MessageBox.d.ts +0 -10
  478. package/dist/ui/components/MessageBox.d.ts.map +0 -1
  479. package/dist/ui/components/MessageDisplay.d.ts +0 -14
  480. package/dist/ui/components/MessageDisplay.d.ts.map +0 -1
  481. package/dist/ui/components/MonitorModeAIPanel.d.ts +0 -23
  482. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +0 -1
  483. package/dist/ui/components/MultiLineInput.d.ts +0 -13
  484. package/dist/ui/components/MultiLineInput.d.ts.map +0 -1
  485. package/dist/ui/components/PasswordPrompt.d.ts +0 -9
  486. package/dist/ui/components/PasswordPrompt.d.ts.map +0 -1
  487. package/dist/ui/components/PlanAcceptedMessage.d.ts +0 -20
  488. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +0 -1
  489. package/dist/ui/components/PlanReviewScreen.d.ts +0 -14
  490. package/dist/ui/components/PlanReviewScreen.d.ts.map +0 -1
  491. package/dist/ui/components/SelectPrompt.d.ts +0 -12
  492. package/dist/ui/components/SelectPrompt.d.ts.map +0 -1
  493. package/dist/ui/components/SlashCommandAutocomplete.d.ts +0 -13
  494. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +0 -1
  495. package/dist/ui/components/StatusBar.d.ts +0 -14
  496. package/dist/ui/components/StatusBar.d.ts.map +0 -1
  497. package/dist/ui/components/StreamingMessageDisplay.d.ts +0 -15
  498. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +0 -1
  499. package/dist/ui/components/TaskCompletedMessage.d.ts +0 -14
  500. package/dist/ui/components/TaskCompletedMessage.d.ts.map +0 -1
  501. package/dist/ui/components/TaskProgressIndicator.d.ts +0 -18
  502. package/dist/ui/components/TaskProgressIndicator.d.ts.map +0 -1
  503. package/dist/ui/components/ThinkingDisplay.d.ts +0 -15
  504. package/dist/ui/components/ThinkingDisplay.d.ts.map +0 -1
  505. package/dist/ui/components/ToolExecutionMessage.d.ts +0 -8
  506. package/dist/ui/components/ToolExecutionMessage.d.ts.map +0 -1
  507. package/dist/ui/components/ToolExecutionStatus.d.ts +0 -10
  508. package/dist/ui/components/ToolExecutionStatus.d.ts.map +0 -1
  509. package/dist/ui/components/ToolResult.d.ts +0 -10
  510. package/dist/ui/components/ToolResult.d.ts.map +0 -1
  511. package/dist/ui/components/VersionUpdatePrompt.d.ts +0 -9
  512. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +0 -1
  513. package/dist/ui/components/WelcomeBanner.d.ts +0 -3
  514. package/dist/ui/components/WelcomeBanner.d.ts.map +0 -1
  515. package/dist/ui/components/WorkflowCreatorScreen.d.ts +0 -25
  516. package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +0 -1
  517. package/dist/utils/ansi-encoder.d.ts +0 -7
  518. package/dist/utils/ansi-encoder.d.ts.map +0 -1
  519. package/dist/utils/chat-formatter.d.ts +0 -12
  520. package/dist/utils/chat-formatter.d.ts.map +0 -1
  521. package/dist/utils/command-history.d.ts +0 -24
  522. package/dist/utils/command-history.d.ts.map +0 -1
  523. package/dist/utils/context-sanitizer.d.ts +0 -50
  524. package/dist/utils/context-sanitizer.d.ts.map +0 -1
  525. package/dist/utils/conversation-logger.d.ts +0 -142
  526. package/dist/utils/conversation-logger.d.ts.map +0 -1
  527. package/dist/utils/custom-commands-manager.d.ts +0 -59
  528. package/dist/utils/custom-commands-manager.d.ts.map +0 -1
  529. package/dist/utils/editor-utils.d.ts +0 -101
  530. package/dist/utils/editor-utils.d.ts.map +0 -1
  531. package/dist/utils/file.d.ts +0 -61
  532. package/dist/utils/file.d.ts.map +0 -1
  533. package/dist/utils/input-classifier.d.ts +0 -25
  534. package/dist/utils/input-classifier.d.ts.map +0 -1
  535. package/dist/utils/logger.d.ts +0 -17
  536. package/dist/utils/logger.d.ts.map +0 -1
  537. package/dist/utils/markdown-parser.d.ts +0 -60
  538. package/dist/utils/markdown-parser.d.ts.map +0 -1
  539. package/dist/utils/shell.d.ts +0 -47
  540. package/dist/utils/shell.d.ts.map +0 -1
  541. package/dist/utils/state.d.ts +0 -13
  542. package/dist/utils/state.d.ts.map +0 -1
  543. package/dist/utils/syntax-checker.d.ts +0 -24
  544. package/dist/utils/syntax-checker.d.ts.map +0 -1
  545. package/dist/utils/terminal-output.d.ts +0 -25
  546. package/dist/utils/terminal-output.d.ts.map +0 -1
  547. package/dist/utils/text-clipboard.d.ts +0 -12
  548. package/dist/utils/text-clipboard.d.ts.map +0 -1
  549. package/dist/utils/unicode-sanitizer.d.ts +0 -44
  550. package/dist/utils/unicode-sanitizer.d.ts.map +0 -1
  551. package/dist/utils/version-checker.d.ts +0 -14
  552. package/dist/utils/version-checker.d.ts.map +0 -1
@@ -1,1172 +1,1113 @@
1
- /**
2
- * SSH Handler for remote shell connections
3
- */
4
- import { Client } from 'ssh2';
5
- import { SubshellConnectionError, SubshellExecutionError } from '../types.js';
6
- import { randomBytes } from 'crypto';
7
- import * as fs from 'fs';
8
- import * as os from 'os';
9
- import * as path from 'path';
10
- import { quickLog } from '../../utils/conversation-logger.js';
11
- /**
12
- * SSH Handler implementation
13
- */
14
- export class SSHHandler {
15
- type = 'ssh';
16
- detectionPatterns = [
17
- /^ssh\s+/,
18
- /^ssh\s+-[^\s]+\s+/,
19
- ];
20
- _client = null;
21
- sftpClient = null;
22
- config = null;
23
- currentWorkingDirectory = '~';
24
- shellType = 'bash';
25
- osType = 'linux';
26
- sessionId = '';
27
- onPasswordRequest;
28
- _isConnected = false;
29
- onDisconnectCallback;
30
- /**
31
- * Get the underlying SSH client (used for PTY commands)
32
- */
33
- get client() {
34
- return this._client;
1
+ import { Client } from "ssh2";
2
+ import { SubshellConnectionError, SubshellExecutionError } from "../types.js";
3
+ import { randomBytes } from "crypto";
4
+ import * as fs from "fs";
5
+ import * as os from "os";
6
+ import * as path from "path";
7
+ import { quickLog } from "../../utils/conversation-logger.js";
8
+ class SSHHandler {
9
+ type = "ssh";
10
+ detectionPatterns = [
11
+ /^ssh\s+/,
12
+ /^ssh\s+-[^\s]+\s+/
13
+ ];
14
+ _client = null;
15
+ sftpClient = null;
16
+ config = null;
17
+ currentWorkingDirectory = "~";
18
+ shellType = "bash";
19
+ osType = "linux";
20
+ sessionId = "";
21
+ onPasswordRequest;
22
+ _isConnected = false;
23
+ onDisconnectCallback;
24
+ /**
25
+ * Get the underlying SSH client (used for PTY commands)
26
+ */
27
+ get client() {
28
+ return this._client;
29
+ }
30
+ /**
31
+ * Set password request callback
32
+ */
33
+ setPasswordRequestCallback(callback) {
34
+ this.onPasswordRequest = callback;
35
+ }
36
+ /**
37
+ * Set callback to be called when the connection is lost
38
+ */
39
+ setDisconnectCallback(callback) {
40
+ this.onDisconnectCallback = callback;
41
+ }
42
+ /**
43
+ * Check if the handler is currently connected
44
+ */
45
+ isConnected() {
46
+ return this._isConnected && this._client !== null;
47
+ }
48
+ /**
49
+ * Detect if a command should trigger this handler
50
+ */
51
+ detect(command) {
52
+ return this.detectionPatterns.some((pattern) => pattern.test(command));
53
+ }
54
+ /**
55
+ * Attach to an existing SSH session detected from PTY output.
56
+ * This is used for "Warpify" mode where the user has already established
57
+ * an SSH connection in the terminal and pressed Alt+E to attach.
58
+ *
59
+ * Unlike connect(), this does NOT establish a new SSH connection.
60
+ * It creates a context based on the detected session info for AI awareness.
61
+ * Commands will continue to flow through the PTY, not via ssh2 library.
62
+ *
63
+ * @param connectionString - The detected connection string (e.g., "user@hostname")
64
+ */
65
+ attachFromPtyOutput(connectionString) {
66
+ this.sessionId = `warpified-${Date.now()}`;
67
+ const parts = connectionString.split("@");
68
+ const username = parts.length > 1 ? parts[0] : "user";
69
+ const hostname = parts.length > 1 ? parts[1] : connectionString;
70
+ this.config = {
71
+ host: hostname,
72
+ port: 22,
73
+ username
74
+ };
75
+ this._isConnected = true;
76
+ return {
77
+ type: "ssh",
78
+ handler: this,
79
+ metadata: {
80
+ hostname,
81
+ username,
82
+ workingDirectory: "~",
83
+ shell: "bash",
84
+ // Assume bash for warpified sessions
85
+ os: "linux",
86
+ port: 22
87
+ },
88
+ connectionState: "connected",
89
+ sessionId: this.sessionId
90
+ };
91
+ }
92
+ /**
93
+ * Connect to the SSH server
94
+ */
95
+ async connect(command, cwd) {
96
+ this.sessionId = randomBytes(16).toString("hex");
97
+ try {
98
+ this.config = this.parseSSHCommand(command);
99
+ await this.establishConnection(this.config);
100
+ try {
101
+ await this.initializeSFTP();
102
+ } catch (e) {
103
+ this.sftpClient = null;
104
+ }
105
+ this.shellType = await this.detectShellType();
106
+ this.osType = await this.detectOSType();
107
+ this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
108
+ await this.injectShellIntegration();
109
+ return {
110
+ type: "ssh",
111
+ handler: this,
112
+ metadata: {
113
+ hostname: this.config.host,
114
+ username: this.config.username,
115
+ workingDirectory: this.currentWorkingDirectory,
116
+ shell: this.shellType,
117
+ os: this.osType,
118
+ port: this.config.port
119
+ },
120
+ connectionState: "connected",
121
+ sessionId: this.sessionId
122
+ };
123
+ } catch (error) {
124
+ throw new SubshellConnectionError(
125
+ "ssh",
126
+ error instanceof Error ? error.message : "Unknown error",
127
+ true
128
+ );
35
129
  }
36
- /**
37
- * Set password request callback
38
- */
39
- setPasswordRequestCallback(callback) {
40
- this.onPasswordRequest = callback;
130
+ }
131
+ /**
132
+ * Connect from an existing remote context (nested session)
133
+ */
134
+ async connectFromRemote(command, cwd, parentContext) {
135
+ this.sessionId = randomBytes(16).toString("hex");
136
+ try {
137
+ this.config = this.parseSSHCommand(command);
138
+ this.config.isNested = true;
139
+ let streamFactory;
140
+ if (parentContext.handler && typeof parentContext.handler.createStream === "function") {
141
+ streamFactory = () => parentContext.handler.createStream(this.config.host, this.config.port);
142
+ } else {
143
+ throw new SubshellConnectionError("ssh", `Parent context (${parentContext.type}) does not support nested connections via tunneling.`, false);
144
+ }
145
+ await this.establishConnection(this.config, void 0, streamFactory);
146
+ try {
147
+ await this.initializeSFTP();
148
+ } catch (e) {
149
+ this.sftpClient = null;
150
+ }
151
+ this.shellType = await this.detectShellType();
152
+ this.osType = await this.detectOSType();
153
+ this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
154
+ await this.injectShellIntegration();
155
+ return {
156
+ type: "ssh",
157
+ handler: this,
158
+ metadata: {
159
+ hostname: this.config.host,
160
+ username: this.config.username,
161
+ workingDirectory: this.currentWorkingDirectory,
162
+ shell: this.shellType,
163
+ os: this.osType,
164
+ port: this.config.port
165
+ },
166
+ connectionState: "connected",
167
+ sessionId: this.sessionId
168
+ };
169
+ } catch (error) {
170
+ throw new SubshellConnectionError(
171
+ "ssh",
172
+ error instanceof Error ? error.message : "Unknown error",
173
+ true
174
+ );
41
175
  }
42
- /**
43
- * Set callback to be called when the connection is lost
44
- */
45
- setDisconnectCallback(callback) {
46
- this.onDisconnectCallback = callback;
176
+ }
177
+ /**
178
+ * Create a network stream/tunnel through this SSH connection
179
+ */
180
+ async createStream(host, port) {
181
+ if (!this._client || !this._isConnected) {
182
+ throw new Error("Not connected to SSH server");
47
183
  }
48
- /**
49
- * Check if the handler is currently connected
50
- */
51
- isConnected() {
52
- return this._isConnected && this._client !== null;
184
+ return new Promise((resolve, reject) => {
185
+ this._client.forwardOut("127.0.0.1", 0, host, port, (err, stream) => {
186
+ if (err) {
187
+ reject(err);
188
+ } else {
189
+ resolve(stream);
190
+ }
191
+ });
192
+ });
193
+ }
194
+ /**
195
+ * Disconnect from the SSH server
196
+ */
197
+ async disconnect() {
198
+ this._isConnected = false;
199
+ if (this.sftpClient) {
200
+ this.sftpClient.end();
201
+ this.sftpClient = null;
53
202
  }
54
- /**
55
- * Detect if a command should trigger this handler
56
- */
57
- detect(command) {
58
- return this.detectionPatterns.some(pattern => pattern.test(command));
203
+ if (this._client) {
204
+ this._client.end();
205
+ this._client = null;
59
206
  }
60
- /**
61
- * Attach to an existing SSH session detected from PTY output.
62
- * This is used for "Warpify" mode where the user has already established
63
- * an SSH connection in the terminal and pressed Alt+E to attach.
64
- *
65
- * Unlike connect(), this does NOT establish a new SSH connection.
66
- * It creates a context based on the detected session info for AI awareness.
67
- * Commands will continue to flow through the PTY, not via ssh2 library.
68
- *
69
- * @param connectionString - The detected connection string (e.g., "user@hostname")
70
- */
71
- attachFromPtyOutput(connectionString) {
72
- this.sessionId = `warpified-${Date.now()}`;
73
- // Parse connection string
74
- const parts = connectionString.split('@');
75
- const username = parts.length > 1 ? parts[0] : 'user';
76
- const hostname = parts.length > 1 ? parts[1] : connectionString;
77
- // Create minimal config for context tracking (no actual SSH connection)
78
- this.config = {
79
- host: hostname,
80
- port: 22,
81
- username: username,
82
- };
83
- // Mark as "connected" for context purposes (PTY handles actual connection)
84
- this._isConnected = true;
85
- return {
86
- type: 'ssh',
87
- handler: this,
88
- metadata: {
89
- hostname: hostname,
90
- username: username,
91
- workingDirectory: '~',
92
- shell: 'bash', // Assume bash for warpified sessions
93
- os: 'linux',
94
- port: 22,
95
- },
96
- connectionState: 'connected',
97
- sessionId: this.sessionId,
98
- };
207
+ this.config = null;
208
+ }
209
+ /**
210
+ * Execute a command in the SSH session
211
+ */
212
+ async executeCommand(command) {
213
+ if (!this._client || !this._isConnected) {
214
+ throw new SubshellExecutionError(command, "Not connected to SSH server");
99
215
  }
100
- /**
101
- * Connect to the SSH server
102
- */
103
- async connect(command, cwd) {
104
- this.sessionId = randomBytes(16).toString('hex');
105
- try {
106
- // Parse SSH command
107
- this.config = this.parseSSHCommand(command);
108
- // Establish SSH connection
109
- await this.establishConnection(this.config);
110
- // Initialize SFTP (non-fatal)
111
- try {
112
- await this.initializeSFTP();
113
- }
114
- catch (e) {
115
- // Continue without SFTP; file ops will fallback to command-based methods
116
- this.sftpClient = null;
117
- }
118
- // Detect shell type and OS
119
- this.shellType = await this.detectShellType();
120
- this.osType = await this.detectOSType();
121
- // Get initial working directory
122
- this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
123
- // Inject shell integration script
124
- await this.injectShellIntegration();
125
- return {
126
- type: 'ssh',
127
- handler: this,
128
- metadata: {
129
- hostname: this.config.host,
130
- username: this.config.username,
131
- workingDirectory: this.currentWorkingDirectory,
132
- shell: this.shellType,
133
- os: this.osType,
134
- port: this.config.port,
135
- },
136
- connectionState: 'connected',
137
- sessionId: this.sessionId,
138
- };
139
- }
140
- catch (error) {
141
- throw new SubshellConnectionError('ssh', error instanceof Error ? error.message : 'Unknown error', true);
142
- }
143
- }
144
- /**
145
- * Connect from an existing remote context (nested session)
146
- */
147
- async connectFromRemote(command, cwd, parentContext) {
148
- this.sessionId = randomBytes(16).toString('hex');
149
- try {
150
- // Parse SSH command
151
- this.config = this.parseSSHCommand(command);
152
- // Mark as nested session to disable implicit local authentication
153
- this.config.isNested = true;
154
- // Check if parent supports streaming
155
- // Check if parent supports streaming
156
- let streamFactory;
157
- if (parentContext.handler && typeof parentContext.handler.createStream === 'function') {
158
- streamFactory = () => parentContext.handler.createStream(this.config.host, this.config.port);
159
- }
160
- else {
161
- throw new SubshellConnectionError('ssh', `Parent context (${parentContext.type}) does not support nested connections via tunneling.`, false);
162
- }
163
- // Establish SSH connection using the tunnel stream factory
164
- await this.establishConnection(this.config, undefined, streamFactory);
165
- // Initialize SFTP (non-fatal)
166
- try {
167
- await this.initializeSFTP();
168
- }
169
- catch (e) {
170
- this.sftpClient = null;
216
+ const pwdTag = `__CENTAURUS_PWD_${this.sessionId}__`;
217
+ return new Promise((resolve, reject) => {
218
+ const timeout = setTimeout(() => {
219
+ reject(new SubshellExecutionError(command, "Command timed out after 30 seconds"));
220
+ }, 3e4);
221
+ this._client.exec(
222
+ `cd "${this.currentWorkingDirectory}" && ${command}; echo "${pwdTag}:$(pwd)"`,
223
+ (err, stream) => {
224
+ if (err) {
225
+ clearTimeout(timeout);
226
+ reject(new SubshellExecutionError(command, err.message));
227
+ return;
228
+ }
229
+ let stdout = "";
230
+ let stderr = "";
231
+ stream.on("close", (code) => {
232
+ clearTimeout(timeout);
233
+ const pwdRegex = new RegExp(`${pwdTag}:(.+)$`, "m");
234
+ const pwdMatch = stdout.match(pwdRegex);
235
+ if (pwdMatch) {
236
+ this.currentWorkingDirectory = pwdMatch[1].trim();
237
+ const removeRegex = new RegExp(`${pwdTag}:.+$`, "m");
238
+ stdout = stdout.replace(removeRegex, "").trim();
171
239
  }
172
- // Detect shell type and OS
173
- this.shellType = await this.detectShellType();
174
- this.osType = await this.detectOSType();
175
- // Get initial working directory
176
- this.currentWorkingDirectory = await this.getCurrentWorkingDirectory();
177
- // Inject shell integration script
178
- await this.injectShellIntegration();
179
- return {
180
- type: 'ssh',
181
- handler: this,
182
- metadata: {
183
- hostname: this.config.host,
184
- username: this.config.username,
185
- workingDirectory: this.currentWorkingDirectory,
186
- shell: this.shellType,
187
- os: this.osType,
188
- port: this.config.port,
189
- },
190
- connectionState: 'connected',
191
- sessionId: this.sessionId,
192
- };
193
- }
194
- catch (error) {
195
- throw new SubshellConnectionError('ssh', error instanceof Error ? error.message : 'Unknown error', true);
196
- }
197
- }
198
- /**
199
- * Create a network stream/tunnel through this SSH connection
200
- */
201
- async createStream(host, port) {
202
- if (!this._client || !this._isConnected) {
203
- throw new Error('Not connected to SSH server');
204
- }
205
- return new Promise((resolve, reject) => {
206
- // Use arbitrary source IP/port (127.0.0.1:0) for the forwardOut call
207
- this._client.forwardOut('127.0.0.1', 0, host, port, (err, stream) => {
208
- if (err) {
209
- reject(err);
210
- }
211
- else {
212
- resolve(stream);
213
- }
240
+ resolve({
241
+ stdout,
242
+ stderr,
243
+ exitCode: code || 0
214
244
  });
245
+ });
246
+ stream.on("data", (data) => {
247
+ stdout += data.toString();
248
+ });
249
+ stream.stderr.on("data", (data) => {
250
+ stderr += data.toString();
251
+ });
252
+ }
253
+ );
254
+ });
255
+ }
256
+ /**
257
+ * Read a file from the remote filesystem
258
+ */
259
+ async readFile(path2) {
260
+ const absolutePath = this.resolveAbsolutePath(path2);
261
+ if (this.sftpClient) {
262
+ return new Promise((resolve, reject) => {
263
+ this.sftpClient.readFile(absolutePath, "utf8", (err, data) => {
264
+ if (err) {
265
+ reject(new Error(`Failed to read file ${path2}: ${err.message}`));
266
+ } else {
267
+ resolve(data);
268
+ }
215
269
  });
270
+ });
216
271
  }
217
- /**
218
- * Disconnect from the SSH server
219
- */
220
- async disconnect() {
221
- // Mark as disconnected first to prevent close/end handlers from firing callback
222
- this._isConnected = false;
223
- if (this.sftpClient) {
224
- this.sftpClient.end();
225
- this.sftpClient = null;
226
- }
227
- if (this._client) {
228
- this._client.end();
229
- this._client = null;
230
- }
231
- this.config = null;
272
+ const result = await this.executeCommand(`base64 "${absolutePath}" || cat "${absolutePath}" | base64`);
273
+ const base64Out = result.stdout.replace(/\s+/g, "");
274
+ try {
275
+ return Buffer.from(base64Out, "base64").toString("utf8");
276
+ } catch (e) {
277
+ throw new Error(`Failed to decode file ${path2}: ${e.message}`);
232
278
  }
233
- /**
234
- * Execute a command in the SSH session
235
- */
236
- async executeCommand(command) {
237
- if (!this._client || !this._isConnected) {
238
- throw new SubshellExecutionError(command, 'Not connected to SSH server');
239
- }
240
- const pwdTag = `__CENTAURUS_PWD_${this.sessionId}__`;
241
- return new Promise((resolve, reject) => {
242
- const timeout = setTimeout(() => {
243
- reject(new SubshellExecutionError(command, 'Command timed out after 30 seconds'));
244
- }, 30000);
245
- this._client.exec(`cd "${this.currentWorkingDirectory}" && ${command}; echo "${pwdTag}:$(pwd)"`, (err, stream) => {
246
- if (err) {
247
- clearTimeout(timeout);
248
- reject(new SubshellExecutionError(command, err.message));
249
- return;
250
- }
251
- let stdout = '';
252
- let stderr = '';
253
- stream.on('close', (code) => {
254
- clearTimeout(timeout);
255
- // Extract working directory from output using unique tag
256
- // We use a constructed regex to match the specific tag for this session
257
- const pwdRegex = new RegExp(`${pwdTag}:(.+)$`, 'm');
258
- const pwdMatch = stdout.match(pwdRegex);
259
- if (pwdMatch) {
260
- this.currentWorkingDirectory = pwdMatch[1].trim();
261
- // Remove the tag line from stdout
262
- const removeRegex = new RegExp(`${pwdTag}:.+$`, 'm');
263
- stdout = stdout.replace(removeRegex, '').trim();
264
- }
265
- resolve({
266
- stdout,
267
- stderr,
268
- exitCode: code || 0,
269
- });
270
- });
271
- stream.on('data', (data) => {
272
- stdout += data.toString();
273
- });
274
- stream.stderr.on('data', (data) => {
275
- stderr += data.toString();
276
- });
277
- });
279
+ }
280
+ /**
281
+ * Write a file to the remote filesystem
282
+ */
283
+ async writeFile(path2, content) {
284
+ const absolutePath = this.resolveAbsolutePath(path2);
285
+ if (this.sftpClient) {
286
+ return new Promise((resolve, reject) => {
287
+ const options = Buffer.isBuffer(content) ? void 0 : "utf8";
288
+ this.sftpClient.writeFile(absolutePath, content, options, (err) => {
289
+ if (err) {
290
+ reject(new Error(`Failed to write file ${path2}: ${err.message}`));
291
+ } else {
292
+ resolve();
293
+ }
278
294
  });
295
+ });
279
296
  }
280
- /**
281
- * Read a file from the remote filesystem
282
- */
283
- async readFile(path) {
284
- const absolutePath = this.resolveAbsolutePath(path);
285
- // Prefer SFTP when available
286
- if (this.sftpClient) {
287
- return new Promise((resolve, reject) => {
288
- this.sftpClient.readFile(absolutePath, 'utf8', (err, data) => {
289
- if (err) {
290
- reject(new Error(`Failed to read file ${path}: ${err.message}`));
291
- }
292
- else {
293
- resolve(data);
294
- }
295
- });
296
- });
297
- }
298
- // Fallback: use base64 over exec
299
- const result = await this.executeCommand(`base64 "${absolutePath}" || cat "${absolutePath}" | base64`);
300
- const base64Out = result.stdout.replace(/\s+/g, '');
301
- try {
302
- return Buffer.from(base64Out, 'base64').toString('utf8');
303
- }
304
- catch (e) {
305
- throw new Error(`Failed to decode file ${path}: ${e.message}`);
306
- }
307
- }
308
- /**
309
- * Write a file to the remote filesystem
310
- */
311
- async writeFile(path, content) {
312
- const absolutePath = this.resolveAbsolutePath(path);
313
- // Prefer SFTP when available
314
- if (this.sftpClient) {
315
- return new Promise((resolve, reject) => {
316
- // ssh2 sftp writeFile supports Buffer or string
317
- // If buffer, encoding is ignored/not needed
318
- const options = Buffer.isBuffer(content) ? undefined : 'utf8';
319
- this.sftpClient.writeFile(absolutePath, content, options, (err) => {
320
- if (err) {
321
- reject(new Error(`Failed to write file ${path}: ${err.message}`));
322
- }
323
- else {
324
- resolve();
325
- }
326
- });
327
- });
328
- }
329
- // Fallback: base64 via exec (robust against special chars), chunked
330
- const inputBuffer = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
331
- const base64Content = inputBuffer.toString('base64');
332
- const CHUNK_SIZE = 32000;
333
- // Truncate file first
334
- let result = await this.executeCommand(`: > "${absolutePath}"`);
335
- if (result.exitCode !== 0) {
336
- throw new Error(`Failed to initialize file ${path}: ${result.stderr || 'unknown error'}`);
337
- }
338
- for (let i = 0; i < base64Content.length; i += CHUNK_SIZE) {
339
- const chunk = base64Content.slice(i, i + CHUNK_SIZE);
340
- result = await this.executeCommand(`echo "${chunk}" | base64 -d >> "${absolutePath}"`);
341
- if (result.exitCode !== 0) {
342
- throw new Error(`Failed to write file chunk for ${path}: ${result.stderr || 'unknown error'}`);
343
- }
344
- }
297
+ const inputBuffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
298
+ const base64Content = inputBuffer.toString("base64");
299
+ const CHUNK_SIZE = 32e3;
300
+ let result = await this.executeCommand(`: > "${absolutePath}"`);
301
+ if (result.exitCode !== 0) {
302
+ throw new Error(`Failed to initialize file ${path2}: ${result.stderr || "unknown error"}`);
345
303
  }
346
- /**
347
- * List directory contents
348
- */
349
- async listDirectory(path) {
350
- const absolutePath = this.resolveAbsolutePath(path);
351
- // Prefer SFTP when available
352
- if (this.sftpClient) {
353
- return new Promise((resolve, reject) => {
354
- this.sftpClient.readdir(absolutePath, (err, list) => {
355
- if (err) {
356
- reject(new Error(`Failed to list directory ${path}: ${err.message}`));
357
- }
358
- else {
359
- const entries = list.map(item => ({
360
- name: item.filename,
361
- type: item.attrs.isDirectory() ? 'directory' : 'file',
362
- size: item.attrs.size,
363
- permissions: item.attrs.mode?.toString(8),
364
- }));
365
- resolve(entries);
366
- }
367
- });
368
- });
369
- }
370
- // Fallback: parse ls -la output
371
- const result = await this.executeCommand(`ls -la "${absolutePath}"`);
372
- if (result.exitCode !== 0) {
373
- throw new Error(`Failed to list directory ${path}: ${result.stderr || 'unknown error'}`);
374
- }
375
- return this.parseDirectoryListing(result.stdout);
304
+ for (let i = 0; i < base64Content.length; i += CHUNK_SIZE) {
305
+ const chunk = base64Content.slice(i, i + CHUNK_SIZE);
306
+ result = await this.executeCommand(`echo "${chunk}" | base64 -d >> "${absolutePath}"`);
307
+ if (result.exitCode !== 0) {
308
+ throw new Error(`Failed to write file chunk for ${path2}: ${result.stderr || "unknown error"}`);
309
+ }
376
310
  }
377
- /**
378
- * Search for files matching a pattern
379
- */
380
- async searchFiles(pattern, directory) {
381
- const absolutePath = this.resolveAbsolutePath(directory);
382
- const escapedPattern = pattern.replace(/'/g, "'\\''");
383
- const command = `grep -rn '${escapedPattern}' "${absolutePath}" 2>/dev/null || true`;
384
- const result = await this.executeCommand(command);
385
- const results = [];
386
- const lines = result.stdout.split('\n').filter(line => line.trim());
387
- for (const line of lines) {
388
- const match = line.match(/^(.+?):(\d+):(.+)$/);
389
- if (match) {
390
- results.push({
391
- file: match[1],
392
- line: parseInt(match[2], 10),
393
- content: match[3],
394
- });
395
- }
396
- }
397
- return results;
311
+ }
312
+ /**
313
+ * List directory contents
314
+ */
315
+ async listDirectory(path2) {
316
+ const absolutePath = this.resolveAbsolutePath(path2);
317
+ if (this.sftpClient) {
318
+ return new Promise((resolve, reject) => {
319
+ this.sftpClient.readdir(absolutePath, (err, list) => {
320
+ if (err) {
321
+ reject(new Error(`Failed to list directory ${path2}: ${err.message}`));
322
+ } else {
323
+ const entries = list.map((item) => ({
324
+ name: item.filename,
325
+ type: item.attrs.isDirectory() ? "directory" : "file",
326
+ size: item.attrs.size,
327
+ permissions: item.attrs.mode?.toString(8)
328
+ }));
329
+ resolve(entries);
330
+ }
331
+ });
332
+ });
398
333
  }
399
- /**
400
- * Get the current working directory
401
- */
402
- async getCurrentWorkingDirectory() {
403
- const result = await this.executeCommand('pwd');
404
- return result.stdout.trim();
334
+ const result = await this.executeCommand(`ls -la "${absolutePath}"`);
335
+ if (result.exitCode !== 0) {
336
+ throw new Error(`Failed to list directory ${path2}: ${result.stderr || "unknown error"}`);
405
337
  }
406
- /**
407
- * Get the shell type
408
- */
409
- async getShellType() {
410
- return this.shellType;
338
+ return this.parseDirectoryListing(result.stdout);
339
+ }
340
+ /**
341
+ * Search for files matching a pattern
342
+ */
343
+ async searchFiles(pattern, directory) {
344
+ const absolutePath = this.resolveAbsolutePath(directory);
345
+ const escapedPattern = pattern.replace(/'/g, "'\\''");
346
+ const command = `grep -rn '${escapedPattern}' "${absolutePath}" 2>/dev/null || true`;
347
+ const result = await this.executeCommand(command);
348
+ const results = [];
349
+ const lines = result.stdout.split("\n").filter((line) => line.trim());
350
+ for (const line of lines) {
351
+ const match = line.match(/^(.+?):(\d+):(.+)$/);
352
+ if (match) {
353
+ results.push({
354
+ file: match[1],
355
+ line: parseInt(match[2], 10),
356
+ content: match[3]
357
+ });
358
+ }
411
359
  }
412
- /**
413
- * Get the operating system type
414
- */
415
- async getOSType() {
416
- return this.osType;
360
+ return results;
361
+ }
362
+ /**
363
+ * Get the current working directory
364
+ */
365
+ async getCurrentWorkingDirectory() {
366
+ const result = await this.executeCommand("pwd");
367
+ return result.stdout.trim();
368
+ }
369
+ /**
370
+ * Get the shell type
371
+ */
372
+ async getShellType() {
373
+ return this.shellType;
374
+ }
375
+ /**
376
+ * Get the operating system type
377
+ */
378
+ async getOSType() {
379
+ return this.osType;
380
+ }
381
+ /**
382
+ * Get breadcrumb information for the UI
383
+ */
384
+ getBreadcrumbs() {
385
+ if (!this.config) {
386
+ return [];
417
387
  }
418
- /**
419
- * Get breadcrumb information for the UI
420
- */
421
- getBreadcrumbs() {
422
- if (!this.config) {
423
- return [];
424
- }
425
- return [
426
- {
427
- label: 'ssh',
428
- color: 'cyan',
388
+ return [
389
+ {
390
+ label: "ssh",
391
+ color: "cyan"
392
+ },
393
+ {
394
+ label: `${this.config.username}@${this.config.host}`,
395
+ color: "cyan"
396
+ }
397
+ ];
398
+ }
399
+ // Private helper methods
400
+ /**
401
+ * Parse SSH command to extract connection details
402
+ */
403
+ parseSSHCommand(command) {
404
+ const parts = this.tokenizeSSHCommand(command);
405
+ let host = "";
406
+ let port = 22;
407
+ let username = "";
408
+ let identityFilePath;
409
+ const optionsWithValue = /* @__PURE__ */ new Set([
410
+ "-b",
411
+ "-c",
412
+ "-D",
413
+ "-E",
414
+ "-F",
415
+ "-I",
416
+ "-J",
417
+ "-L",
418
+ "-l",
419
+ "-m",
420
+ "-O",
421
+ "-o",
422
+ "-p",
423
+ "-Q",
424
+ "-R",
425
+ "-S",
426
+ "-W",
427
+ "-w"
428
+ ]);
429
+ for (let i = 1; i < parts.length; i++) {
430
+ const part = parts[i];
431
+ if (part === "--") {
432
+ if (!host && i + 1 < parts.length) {
433
+ host = parts[i + 1];
434
+ }
435
+ break;
436
+ }
437
+ if (part === "-p" && i + 1 < parts.length) {
438
+ port = this.parsePort(parts[i + 1], port);
439
+ i++;
440
+ } else if (part.startsWith("-p") && part.length > 2) {
441
+ port = this.parsePort(part.slice(2), port);
442
+ } else if (part === "-l" && i + 1 < parts.length) {
443
+ username = parts[i + 1];
444
+ i++;
445
+ } else if (part.startsWith("-l") && part.length > 2) {
446
+ username = part.slice(2);
447
+ } else if (part === "-i" && i + 1 < parts.length) {
448
+ identityFilePath = parts[i + 1];
449
+ i++;
450
+ } else if (part.startsWith("-i") && part.length > 2) {
451
+ identityFilePath = part.slice(2);
452
+ } else if (part === "-o" && i + 1 < parts.length) {
453
+ const optionToken = parts[i + 1];
454
+ if (optionToken.includes("=")) {
455
+ this.applySSHOption(
456
+ optionToken,
457
+ (value) => {
458
+ port = value;
459
+ },
460
+ (value) => {
461
+ username = value;
429
462
  },
430
- {
431
- label: `${this.config.username}@${this.config.host}`,
432
- color: 'cyan',
463
+ (value) => {
464
+ identityFilePath = value;
433
465
  },
434
- ];
466
+ port
467
+ );
468
+ i++;
469
+ } else if (i + 2 < parts.length) {
470
+ this.applySSHOption(
471
+ `${optionToken}=${parts[i + 2]}`,
472
+ (value) => {
473
+ port = value;
474
+ },
475
+ (value) => {
476
+ username = value;
477
+ },
478
+ (value) => {
479
+ identityFilePath = value;
480
+ },
481
+ port
482
+ );
483
+ i += 2;
484
+ } else {
485
+ i++;
486
+ }
487
+ } else if (part.startsWith("-o") && part.length > 2) {
488
+ this.applySSHOption(part.slice(2), (value) => {
489
+ port = value;
490
+ }, (value) => {
491
+ username = value;
492
+ }, (value) => {
493
+ identityFilePath = value;
494
+ }, port);
495
+ } else if (part.startsWith("-") && part !== "-p") {
496
+ if (optionsWithValue.has(part) && i + 1 < parts.length) {
497
+ i++;
498
+ }
499
+ continue;
500
+ } else if (!host) {
501
+ if (part.includes("@")) {
502
+ const atIndex = part.indexOf("@");
503
+ const user = part.slice(0, atIndex);
504
+ const hostname = part.slice(atIndex + 1);
505
+ if (user) {
506
+ username = user;
507
+ }
508
+ host = hostname;
509
+ } else {
510
+ host = part;
511
+ }
512
+ }
435
513
  }
436
- // Private helper methods
437
- /**
438
- * Parse SSH command to extract connection details
439
- */
440
- parseSSHCommand(command) {
441
- const parts = this.tokenizeSSHCommand(command);
442
- let host = '';
443
- let port = 22;
444
- let username = '';
445
- let identityFilePath;
446
- // SSH options that consume the next token as a value.
447
- // We skip those values so they are not mistaken as the host.
448
- const optionsWithValue = new Set([
449
- '-b', '-c', '-D', '-E', '-F', '-I', '-J', '-L', '-l', '-m', '-O', '-o', '-p', '-Q', '-R', '-S', '-W', '-w'
450
- ]);
451
- // Parse command line arguments
452
- for (let i = 1; i < parts.length; i++) {
453
- const part = parts[i];
454
- if (part === '--') {
455
- // End of options; next token is the host
456
- if (!host && i + 1 < parts.length) {
457
- host = parts[i + 1];
458
- }
459
- break;
460
- }
461
- if (part === '-p' && i + 1 < parts.length) {
462
- port = this.parsePort(parts[i + 1], port);
463
- i++;
464
- }
465
- else if (part.startsWith('-p') && part.length > 2) {
466
- port = this.parsePort(part.slice(2), port);
467
- }
468
- else if (part === '-l' && i + 1 < parts.length) {
469
- username = parts[i + 1];
470
- i++;
471
- }
472
- else if (part.startsWith('-l') && part.length > 2) {
473
- username = part.slice(2);
474
- }
475
- else if (part === '-i' && i + 1 < parts.length) {
476
- identityFilePath = parts[i + 1];
477
- i++;
478
- }
479
- else if (part.startsWith('-i') && part.length > 2) {
480
- identityFilePath = part.slice(2);
481
- }
482
- else if (part === '-o' && i + 1 < parts.length) {
483
- // Support both "-o Key=Value" and "-o Key Value" styles.
484
- const optionToken = parts[i + 1];
485
- if (optionToken.includes('=')) {
486
- this.applySSHOption(optionToken, (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
487
- i++;
488
- }
489
- else if (i + 2 < parts.length) {
490
- this.applySSHOption(`${optionToken}=${parts[i + 2]}`, (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
491
- i += 2;
492
- }
493
- else {
494
- i++;
495
- }
496
- }
497
- else if (part.startsWith('-o') && part.length > 2) {
498
- this.applySSHOption(part.slice(2), (value) => { port = value; }, (value) => { username = value; }, (value) => { identityFilePath = value; }, port);
499
- }
500
- else if (part.startsWith('-') && part !== '-p') {
501
- // Skip flags and their value, if any
502
- if (optionsWithValue.has(part) && i + 1 < parts.length) {
503
- i++;
504
- }
505
- continue;
506
- }
507
- else if (!host) {
508
- // This should be the host (possibly with username)
509
- if (part.includes('@')) {
510
- const atIndex = part.indexOf('@');
511
- const user = part.slice(0, atIndex);
512
- const hostname = part.slice(atIndex + 1);
513
- if (user) {
514
- username = user;
515
- }
516
- host = hostname;
517
- }
518
- else {
519
- host = part;
520
- }
521
- }
522
- }
523
- // Default username to current user if not specified
524
- if (!username) {
525
- username = process.env.USER || process.env.USERNAME || 'user';
526
- }
527
- if (!host) {
528
- throw new Error('Could not parse SSH host from command');
529
- }
530
- const parsedConfig = {
531
- host,
532
- port,
533
- username,
534
- };
535
- if (identityFilePath) {
536
- parsedConfig.privateKey = this.loadIdentityFile(identityFilePath);
537
- }
538
- return parsedConfig;
514
+ if (!username) {
515
+ username = process.env.USER || process.env.USERNAME || "user";
516
+ }
517
+ if (!host) {
518
+ throw new Error("Could not parse SSH host from command");
519
+ }
520
+ const parsedConfig = {
521
+ host,
522
+ port,
523
+ username
524
+ };
525
+ if (identityFilePath) {
526
+ parsedConfig.privateKey = this.loadIdentityFile(identityFilePath);
539
527
  }
540
- /**
541
- * Establish SSH connection
542
- */
543
- /**
544
- * Establish SSH connection.
545
- * Handles authentication retries (agent -> keys -> password).
546
- * For nested connections, uses streamFactory to get a fresh stream for each attempt.
547
- */
548
- async establishConnection(config, stream, streamFactory) {
549
- let promptedForPassword = false;
550
- let firstError;
551
- quickLog(`[SSH Auth] Starting auth for ${config.username}@${config.host}:${config.port}\n`);
552
- quickLog(`[SSH Auth] Has privateKey: ${!!config.privateKey}, Has password: ${!!config.password}\n`);
528
+ return parsedConfig;
529
+ }
530
+ /**
531
+ * Establish SSH connection
532
+ */
533
+ /**
534
+ * Establish SSH connection.
535
+ * Handles authentication retries (agent -> keys -> password).
536
+ * For nested connections, uses streamFactory to get a fresh stream for each attempt.
537
+ */
538
+ async establishConnection(config, stream, streamFactory) {
539
+ let promptedForPassword = false;
540
+ let firstError;
541
+ quickLog(`[SSH Auth] Starting auth for ${config.username}@${config.host}:${config.port}
542
+ `);
543
+ quickLog(`[SSH Auth] Has privateKey: ${!!config.privateKey}, Has password: ${!!config.password}
544
+ `);
545
+ try {
546
+ quickLog(`[SSH Auth] Attempt 1: agent/none (tryKeyboard=false)
547
+ `);
548
+ const currentStream = streamFactory ? await streamFactory() : stream;
549
+ await this.establishConnectionOnce(config, currentStream, false, () => {
550
+ promptedForPassword = true;
551
+ });
552
+ quickLog(`[SSH Auth] Attempt 1 succeeded (agent or none auth)
553
+ `);
554
+ return;
555
+ } catch (error) {
556
+ const errMsg = error instanceof Error ? error.message : String(error);
557
+ quickLog(`[SSH Auth] Attempt 1 failed: ${errMsg}, promptedForPassword=${promptedForPassword}
558
+ `);
559
+ firstError = error;
560
+ if (!this.isAuthenticationFailure(error) || promptedForPassword) {
561
+ quickLog(`[SSH Auth] Stopping early: isAuthFailure=${this.isAuthenticationFailure(error)}, prompted=${promptedForPassword}
562
+ `);
563
+ throw error;
564
+ }
565
+ }
566
+ if (!config.privateKey && !config.isNested) {
567
+ const defaultKeys = this.loadDefaultIdentityFiles(config.host);
568
+ quickLog(`[SSH Auth] Found ${defaultKeys.length} default identity file(s)
569
+ `);
570
+ for (let ki = 0; ki < defaultKeys.length; ki++) {
571
+ const key = defaultKeys[ki];
553
572
  try {
554
- quickLog(`[SSH Auth] Attempt 1: agent/none (tryKeyboard=false)\n`);
555
- // Get fresh stream if factory provided
556
- const currentStream = streamFactory ? await streamFactory() : stream;
557
- await this.establishConnectionOnce(config, currentStream, false, () => {
558
- promptedForPassword = true;
559
- });
560
- quickLog(`[SSH Auth] Attempt 1 succeeded (agent or none auth)\n`);
561
- return;
562
- }
563
- catch (error) {
564
- const errMsg = error instanceof Error ? error.message : String(error);
565
- quickLog(`[SSH Auth] Attempt 1 failed: ${errMsg}, promptedForPassword=${promptedForPassword}\n`);
566
- firstError = error;
567
- // Stop early for non-auth failures or if user was already prompted.
568
- if (!this.isAuthenticationFailure(error) || promptedForPassword) {
569
- quickLog(`[SSH Auth] Stopping early: isAuthFailure=${this.isAuthenticationFailure(error)}, prompted=${promptedForPassword}\n`);
570
- throw error;
571
- }
572
- }
573
- // If no explicit key was provided, try default identity files before prompting for password.
574
- // Skip this for nested sessions to avoid using local keys where remote ones are expected.
575
- if (!config.privateKey && !config.isNested) {
576
- const defaultKeys = this.loadDefaultIdentityFiles(config.host);
577
- quickLog(`[SSH Auth] Found ${defaultKeys.length} default identity file(s)\n`);
578
- for (let ki = 0; ki < defaultKeys.length; ki++) {
579
- const key = defaultKeys[ki];
580
- try {
581
- quickLog(`[SSH Auth] Trying key file ${ki + 1}/${defaultKeys.length} (${key.length} bytes)\n`);
582
- // Get fresh stream if factory provided
583
- const keyStream = streamFactory ? await streamFactory() : stream;
584
- const keyConfig = {
585
- ...config,
586
- privateKey: key,
587
- password: undefined,
588
- };
589
- await this.establishConnectionOnce(keyConfig, keyStream, false, () => {
590
- promptedForPassword = true;
591
- });
592
- quickLog(`[SSH Auth] Key file ${ki + 1} succeeded!\n`);
593
- config.privateKey = key;
594
- return;
595
- }
596
- catch (keyError) {
597
- const keyErrMsg = keyError instanceof Error ? keyError.message : String(keyError);
598
- quickLog(`[SSH Auth] Key file ${ki + 1} failed: ${keyErrMsg}\n`);
599
- if (promptedForPassword) {
600
- // User already provided a secret once for this connection attempt.
601
- throw keyError;
602
- }
603
- // Continue trying other keys for expected auth/key-format failures.
604
- if (this.isAuthenticationFailure(keyError) || this.isKeyParseFailure(keyError)) {
605
- continue;
606
- }
607
- throw keyError;
608
- }
609
- }
610
- }
611
- else {
612
- quickLog(`[SSH Auth] Explicit key provided, skipping default key search\n`);
613
- }
614
- // Password retry (only if callback exists).
615
- quickLog(`[SSH Auth] All key-based auth methods exhausted, falling back to password\n`);
616
- if (!this.onPasswordRequest) {
617
- throw (firstError instanceof Error ? firstError : new Error('SSH authentication failed'));
618
- }
619
- const password = await this.requestPassword(config);
620
- config.password = password;
621
- config.privateKey = undefined;
622
- // Get fresh stream for password attempt
623
- const passwordStream = streamFactory ? await streamFactory() : stream;
624
- await this.establishConnectionOnce(config, passwordStream, true, () => {
573
+ quickLog(`[SSH Auth] Trying key file ${ki + 1}/${defaultKeys.length} (${key.length} bytes)
574
+ `);
575
+ const keyStream = streamFactory ? await streamFactory() : stream;
576
+ const keyConfig = {
577
+ ...config,
578
+ privateKey: key,
579
+ password: void 0
580
+ };
581
+ await this.establishConnectionOnce(keyConfig, keyStream, false, () => {
625
582
  promptedForPassword = true;
626
- });
583
+ });
584
+ quickLog(`[SSH Auth] Key file ${ki + 1} succeeded!
585
+ `);
586
+ config.privateKey = key;
587
+ return;
588
+ } catch (keyError) {
589
+ const keyErrMsg = keyError instanceof Error ? keyError.message : String(keyError);
590
+ quickLog(`[SSH Auth] Key file ${ki + 1} failed: ${keyErrMsg}
591
+ `);
592
+ if (promptedForPassword) {
593
+ throw keyError;
594
+ }
595
+ if (this.isAuthenticationFailure(keyError) || this.isKeyParseFailure(keyError)) {
596
+ continue;
597
+ }
598
+ throw keyError;
599
+ }
600
+ }
601
+ } else {
602
+ quickLog(`[SSH Auth] Explicit key provided, skipping default key search
603
+ `);
627
604
  }
628
- /**
629
- * Single SSH connection attempt.
630
- * Password retry logic is handled by establishConnection().
631
- * @param enableKeyboard - Whether to enable keyboard-interactive auth.
632
- * Set to false for key-based attempts so the server doesn't prompt for password prematurely.
633
- * Set to true only for the final password-retry attempt.
634
- */
635
- async establishConnectionOnce(config, stream, enableKeyboard, onPasswordPrompt) {
636
- return new Promise((resolve, reject) => {
637
- const client = new Client();
638
- this._client = client;
639
- let settled = false;
640
- const settleResolve = () => {
641
- if (settled) {
642
- return;
643
- }
644
- settled = true;
645
- this._isConnected = true;
646
- resolve();
647
- };
648
- const settleReject = (error) => {
649
- if (settled) {
650
- return;
651
- }
652
- settled = true;
653
- this._isConnected = false;
654
- this._client = null;
655
- try {
656
- client.end();
657
- }
658
- catch {
659
- // Ignore cleanup errors
660
- }
661
- reject(error);
662
- };
663
- client.on('ready', () => {
664
- settleResolve();
665
- });
666
- client.on('error', (err) => {
667
- settleReject(new Error(`ssh connection failed: ${err.message}`));
668
- });
669
- client.on('keyboard-interactive', async (_name, _instructions, _lang, prompts, finish) => {
670
- if (!prompts || prompts.length === 0 || !this.onPasswordRequest) {
671
- finish([]);
672
- return;
673
- }
674
- try {
675
- const responses = [];
676
- for (const prompt of prompts) {
677
- onPasswordPrompt();
678
- const message = this.buildPasswordPromptMessage(config, prompt.prompt);
679
- const response = await this.requestPassword(config, message);
680
- responses.push(response);
681
- if (!prompt.echo) {
682
- config.password = response;
683
- }
684
- }
685
- finish(responses);
686
- }
687
- catch (error) {
688
- finish([]);
689
- settleReject(error instanceof Error ? error : new Error('Password input cancelled'));
690
- }
691
- });
692
- // Listen for unexpected disconnections
693
- client.on('close', () => {
694
- if (this._isConnected) {
695
- // Unexpected disconnect (not initiated by us calling disconnect())
696
- this._isConnected = false;
697
- this._client = null;
698
- if (this.onDisconnectCallback) {
699
- this.onDisconnectCallback('Connection closed by server');
700
- }
701
- }
702
- });
703
- client.on('end', () => {
704
- if (this._isConnected) {
705
- // Unexpected disconnect
706
- this._isConnected = false;
707
- this._client = null;
708
- if (this.onDisconnectCallback) {
709
- this.onDisconnectCallback('Connection ended');
710
- }
711
- }
712
- });
713
- client.connect(this.buildConnectConfig(config, stream, enableKeyboard));
714
- });
605
+ quickLog(`[SSH Auth] All key-based auth methods exhausted, falling back to password
606
+ `);
607
+ if (!this.onPasswordRequest) {
608
+ throw firstError instanceof Error ? firstError : new Error("SSH authentication failed");
715
609
  }
716
- /**
717
- * Build ssh2 connection config.
718
- * @param enableKeyboard - When true, enables keyboard-interactive auth.
719
- * This should only be true for the final password-retry attempt;
720
- * otherwise the server's keyboard-interactive challenge fires before
721
- * key-based auth methods (agent, key files) have been tried.
722
- */
723
- buildConnectConfig(config, stream, enableKeyboard = false) {
724
- const connectConfig = {
725
- host: config.host,
726
- port: config.port,
727
- username: config.username,
728
- tryKeyboard: enableKeyboard,
729
- };
730
- if (stream) {
731
- connectConfig.sock = stream;
732
- }
733
- // Skip local SSH agent for nested sessions
734
- if (!config.isNested) {
735
- if (process.env.SSH_AUTH_SOCK) {
736
- connectConfig.agent = process.env.SSH_AUTH_SOCK;
737
- quickLog(`[SSH Auth] Using SSH_AUTH_SOCK agent: ${process.env.SSH_AUTH_SOCK}\n`);
738
- }
739
- else if (os.platform() === 'win32') {
740
- // Windows OpenSSH agent uses a named pipe, not SSH_AUTH_SOCK.
741
- // The ssh2 library supports this via the agent option.
742
- // Pipe path: \\.\pipe\openssh-ssh-agent
743
- const winPipe = ['', '', '.', 'pipe', 'openssh-ssh-agent'].join('\\');
744
- quickLog(`[SSH Auth] Windows detected, checking agent pipe: ${winPipe}\n`);
745
- try {
746
- // Did not stat the pipe as it returns EBUSY on Windows sometimes
747
- // Just try to use it
748
- connectConfig.agent = winPipe;
749
- quickLog(`[SSH Auth] Using Windows agent pipe (blind trust)\n`);
750
- }
751
- catch (e) {
752
- // Named pipe not available (agent service not running).
753
- // Fall through to key file / password auth.
754
- quickLog(`[SSH Auth] Windows agent pipe error: ${e.message}\n`);
755
- }
756
- }
757
- else {
758
- quickLog(`[SSH Auth] No SSH agent available (no SSH_AUTH_SOCK, not Windows)\n`);
759
- }
610
+ const password = await this.requestPassword(config);
611
+ config.password = password;
612
+ config.privateKey = void 0;
613
+ const passwordStream = streamFactory ? await streamFactory() : stream;
614
+ await this.establishConnectionOnce(config, passwordStream, true, () => {
615
+ promptedForPassword = true;
616
+ });
617
+ }
618
+ /**
619
+ * Single SSH connection attempt.
620
+ * Password retry logic is handled by establishConnection().
621
+ * @param enableKeyboard - Whether to enable keyboard-interactive auth.
622
+ * Set to false for key-based attempts so the server doesn't prompt for password prematurely.
623
+ * Set to true only for the final password-retry attempt.
624
+ */
625
+ async establishConnectionOnce(config, stream, enableKeyboard, onPasswordPrompt) {
626
+ return new Promise((resolve, reject) => {
627
+ const client = new Client();
628
+ this._client = client;
629
+ let settled = false;
630
+ const settleResolve = () => {
631
+ if (settled) {
632
+ return;
633
+ }
634
+ settled = true;
635
+ this._isConnected = true;
636
+ resolve();
637
+ };
638
+ const settleReject = (error) => {
639
+ if (settled) {
640
+ return;
760
641
  }
761
- if (config.password) {
762
- connectConfig.password = config.password;
642
+ settled = true;
643
+ this._isConnected = false;
644
+ this._client = null;
645
+ try {
646
+ client.end();
647
+ } catch {
648
+ }
649
+ reject(error);
650
+ };
651
+ client.on("ready", () => {
652
+ settleResolve();
653
+ });
654
+ client.on("error", (err) => {
655
+ settleReject(new Error(`ssh connection failed: ${err.message}`));
656
+ });
657
+ client.on("keyboard-interactive", async (_name, _instructions, _lang, prompts, finish) => {
658
+ if (!prompts || prompts.length === 0 || !this.onPasswordRequest) {
659
+ finish([]);
660
+ return;
763
661
  }
764
- if (config.privateKey) {
765
- connectConfig.privateKey = config.privateKey;
766
- if (config.passphrase) {
767
- connectConfig.passphrase = config.passphrase;
662
+ try {
663
+ const responses = [];
664
+ for (const prompt of prompts) {
665
+ onPasswordPrompt();
666
+ const message = this.buildPasswordPromptMessage(config, prompt.prompt);
667
+ const response = await this.requestPassword(config, message);
668
+ responses.push(response);
669
+ if (!prompt.echo) {
670
+ config.password = response;
768
671
  }
769
- }
770
- // Add keepalive settings to prevent connection drops by NATs/Firewalls (e.g. GCP)
771
- connectConfig.keepaliveInterval = 10000; // 10 seconds
772
- connectConfig.keepaliveCountMax = 3;
773
- return connectConfig;
672
+ }
673
+ finish(responses);
674
+ } catch (error) {
675
+ finish([]);
676
+ settleReject(error instanceof Error ? error : new Error("Password input cancelled"));
677
+ }
678
+ });
679
+ client.on("close", () => {
680
+ if (this._isConnected) {
681
+ this._isConnected = false;
682
+ this._client = null;
683
+ if (this.onDisconnectCallback) {
684
+ this.onDisconnectCallback("Connection closed by server");
685
+ }
686
+ }
687
+ });
688
+ client.on("end", () => {
689
+ if (this._isConnected) {
690
+ this._isConnected = false;
691
+ this._client = null;
692
+ if (this.onDisconnectCallback) {
693
+ this.onDisconnectCallback("Connection ended");
694
+ }
695
+ }
696
+ });
697
+ client.connect(this.buildConnectConfig(config, stream, enableKeyboard));
698
+ });
699
+ }
700
+ /**
701
+ * Build ssh2 connection config.
702
+ * @param enableKeyboard - When true, enables keyboard-interactive auth.
703
+ * This should only be true for the final password-retry attempt;
704
+ * otherwise the server's keyboard-interactive challenge fires before
705
+ * key-based auth methods (agent, key files) have been tried.
706
+ */
707
+ buildConnectConfig(config, stream, enableKeyboard = false) {
708
+ const connectConfig = {
709
+ host: config.host,
710
+ port: config.port,
711
+ username: config.username,
712
+ tryKeyboard: enableKeyboard
713
+ };
714
+ if (stream) {
715
+ connectConfig.sock = stream;
774
716
  }
775
- async requestPassword(config, promptMessage) {
776
- if (!this.onPasswordRequest) {
777
- throw new Error('Password authentication required but no password prompt handler is configured');
778
- }
717
+ if (!config.isNested) {
718
+ if (process.env.SSH_AUTH_SOCK) {
719
+ connectConfig.agent = process.env.SSH_AUTH_SOCK;
720
+ quickLog(`[SSH Auth] Using SSH_AUTH_SOCK agent: ${process.env.SSH_AUTH_SOCK}
721
+ `);
722
+ } else if (os.platform() === "win32") {
723
+ const winPipe = ["", "", ".", "pipe", "openssh-ssh-agent"].join("\\");
724
+ quickLog(`[SSH Auth] Windows detected, checking agent pipe: ${winPipe}
725
+ `);
779
726
  try {
780
- return await this.onPasswordRequest(promptMessage || `Password for ${config.username}@${config.host}:`);
781
- }
782
- catch {
783
- throw new Error('Password input cancelled');
784
- }
727
+ connectConfig.agent = winPipe;
728
+ quickLog(`[SSH Auth] Using Windows agent pipe (blind trust)
729
+ `);
730
+ } catch (e) {
731
+ quickLog(`[SSH Auth] Windows agent pipe error: ${e.message}
732
+ `);
733
+ }
734
+ } else {
735
+ quickLog(`[SSH Auth] No SSH agent available (no SSH_AUTH_SOCK, not Windows)
736
+ `);
737
+ }
785
738
  }
786
- buildPasswordPromptMessage(config, rawPrompt) {
787
- const trimmedPrompt = rawPrompt?.trim();
788
- if (trimmedPrompt) {
789
- return trimmedPrompt;
790
- }
791
- return `Password for ${config.username}@${config.host}:`;
739
+ if (config.password) {
740
+ connectConfig.password = config.password;
792
741
  }
793
- isAuthenticationFailure(error) {
794
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
795
- return (message.includes('all configured authentication methods failed') ||
796
- message.includes('authentication failed') ||
797
- message.includes('permission denied'));
742
+ if (config.privateKey) {
743
+ connectConfig.privateKey = config.privateKey;
744
+ if (config.passphrase) {
745
+ connectConfig.passphrase = config.passphrase;
746
+ }
798
747
  }
799
- isKeyParseFailure(error) {
800
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
801
- return (message.includes('cannot parse privatekey') ||
802
- message.includes('bad passphrase') ||
803
- message.includes('no passphrase given') ||
804
- message.includes('invalid private key'));
748
+ connectConfig.keepaliveInterval = 1e4;
749
+ connectConfig.keepaliveCountMax = 3;
750
+ return connectConfig;
751
+ }
752
+ async requestPassword(config, promptMessage) {
753
+ if (!this.onPasswordRequest) {
754
+ throw new Error("Password authentication required but no password prompt handler is configured");
805
755
  }
806
- parsePort(value, fallbackPort) {
807
- const parsed = Number.parseInt(value, 10);
808
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackPort;
756
+ try {
757
+ return await this.onPasswordRequest(
758
+ promptMessage || `Password for ${config.username}@${config.host}:`
759
+ );
760
+ } catch {
761
+ throw new Error("Password input cancelled");
809
762
  }
810
- applySSHOption(option, setPort, setUsername, setIdentityFile, fallbackPort) {
811
- const trimmed = option.trim();
812
- if (!trimmed) {
813
- return;
814
- }
815
- const equalsIndex = trimmed.indexOf('=');
816
- const key = (equalsIndex >= 0 ? trimmed.slice(0, equalsIndex) : trimmed).trim().toLowerCase();
817
- const value = (equalsIndex >= 0 ? trimmed.slice(equalsIndex + 1) : '').trim();
818
- if (!value) {
819
- return;
820
- }
821
- if (key === 'identityfile') {
822
- setIdentityFile(value);
823
- return;
824
- }
825
- if (key === 'user') {
826
- setUsername(value);
827
- return;
828
- }
829
- if (key === 'port') {
830
- setPort(this.parsePort(value, fallbackPort));
831
- }
763
+ }
764
+ buildPasswordPromptMessage(config, rawPrompt) {
765
+ const trimmedPrompt = rawPrompt?.trim();
766
+ if (trimmedPrompt) {
767
+ return trimmedPrompt;
832
768
  }
833
- tokenizeSSHCommand(command) {
834
- const tokens = [];
835
- let current = '';
836
- let quote = null;
837
- let escaped = false;
838
- for (const char of command.trim()) {
839
- if (escaped) {
840
- current += char;
841
- escaped = false;
842
- continue;
843
- }
844
- if (char === '\\' && quote !== '\'') {
845
- escaped = true;
846
- continue;
847
- }
848
- if (quote) {
849
- if (char === quote) {
850
- quote = null;
851
- }
852
- else {
853
- current += char;
854
- }
855
- continue;
856
- }
857
- if (char === '"' || char === '\'') {
858
- quote = char;
859
- continue;
860
- }
861
- if (/\s/.test(char)) {
862
- if (current) {
863
- tokens.push(current);
864
- current = '';
865
- }
866
- continue;
867
- }
868
- current += char;
869
- }
769
+ return `Password for ${config.username}@${config.host}:`;
770
+ }
771
+ isAuthenticationFailure(error) {
772
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
773
+ return message.includes("all configured authentication methods failed") || message.includes("authentication failed") || message.includes("permission denied");
774
+ }
775
+ isKeyParseFailure(error) {
776
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
777
+ return message.includes("cannot parse privatekey") || message.includes("bad passphrase") || message.includes("no passphrase given") || message.includes("invalid private key");
778
+ }
779
+ parsePort(value, fallbackPort) {
780
+ const parsed = Number.parseInt(value, 10);
781
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackPort;
782
+ }
783
+ applySSHOption(option, setPort, setUsername, setIdentityFile, fallbackPort) {
784
+ const trimmed = option.trim();
785
+ if (!trimmed) {
786
+ return;
787
+ }
788
+ const equalsIndex = trimmed.indexOf("=");
789
+ const key = (equalsIndex >= 0 ? trimmed.slice(0, equalsIndex) : trimmed).trim().toLowerCase();
790
+ const value = (equalsIndex >= 0 ? trimmed.slice(equalsIndex + 1) : "").trim();
791
+ if (!value) {
792
+ return;
793
+ }
794
+ if (key === "identityfile") {
795
+ setIdentityFile(value);
796
+ return;
797
+ }
798
+ if (key === "user") {
799
+ setUsername(value);
800
+ return;
801
+ }
802
+ if (key === "port") {
803
+ setPort(this.parsePort(value, fallbackPort));
804
+ }
805
+ }
806
+ tokenizeSSHCommand(command) {
807
+ const tokens = [];
808
+ let current = "";
809
+ let quote = null;
810
+ let escaped = false;
811
+ for (const char of command.trim()) {
812
+ if (escaped) {
813
+ current += char;
814
+ escaped = false;
815
+ continue;
816
+ }
817
+ if (char === "\\" && quote !== "'") {
818
+ escaped = true;
819
+ continue;
820
+ }
821
+ if (quote) {
822
+ if (char === quote) {
823
+ quote = null;
824
+ } else {
825
+ current += char;
826
+ }
827
+ continue;
828
+ }
829
+ if (char === '"' || char === "'") {
830
+ quote = char;
831
+ continue;
832
+ }
833
+ if (/\s/.test(char)) {
870
834
  if (current) {
871
- tokens.push(current);
835
+ tokens.push(current);
836
+ current = "";
872
837
  }
873
- return tokens;
838
+ continue;
839
+ }
840
+ current += char;
874
841
  }
875
- loadIdentityFile(identityPath) {
876
- const resolvedPath = this.resolveIdentityPath(identityPath);
877
- try {
878
- return fs.readFileSync(resolvedPath);
879
- }
880
- catch (error) {
881
- if (error?.code === 'ENOENT') {
882
- throw new Error(`SSH identity file not found: ${resolvedPath}`);
883
- }
884
- throw new Error(`Failed to read SSH identity file "${resolvedPath}": ${error?.message || 'Unknown error'}`);
885
- }
842
+ if (current) {
843
+ tokens.push(current);
886
844
  }
887
- resolveIdentityPath(identityPath) {
888
- const trimmed = identityPath.trim();
889
- if (!trimmed) {
890
- return trimmed;
891
- }
892
- if (trimmed.startsWith('~')) {
893
- const homeDir = os.homedir();
894
- return path.resolve(homeDir, trimmed.slice(1));
895
- }
896
- return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
845
+ return tokens;
846
+ }
847
+ loadIdentityFile(identityPath) {
848
+ const resolvedPath = this.resolveIdentityPath(identityPath);
849
+ try {
850
+ return fs.readFileSync(resolvedPath);
851
+ } catch (error) {
852
+ if (error?.code === "ENOENT") {
853
+ throw new Error(`SSH identity file not found: ${resolvedPath}`);
854
+ }
855
+ throw new Error(`Failed to read SSH identity file "${resolvedPath}": ${error?.message || "Unknown error"}`);
897
856
  }
898
- loadDefaultIdentityFiles(host) {
899
- const homeDir = os.homedir();
900
- const candidatePaths = [
901
- path.join(homeDir, '.ssh', 'id_ed25519'),
902
- path.join(homeDir, '.ssh', 'id_rsa'),
903
- path.join(homeDir, '.ssh', 'id_ecdsa'),
904
- path.join(homeDir, '.ssh', 'id_dsa'),
905
- ];
906
- // Add keys from ~/.ssh/config if host is provided
907
- if (host) {
908
- const configKeys = this.getIdentityFilesFromConfig(host);
909
- quickLog(`[SSH Config] Found ${configKeys.length} keys in config for host ${host}\n`);
910
- for (const keyPath of configKeys) {
911
- // Resolving relative paths in config (relative to ~/.ssh)
912
- let resolvedKeyPath = keyPath;
913
- if (!path.isAbsolute(keyPath)) {
914
- if (keyPath.startsWith('~')) {
915
- resolvedKeyPath = keyPath.replace(/^~/, homeDir);
916
- }
917
- else {
918
- resolvedKeyPath = path.join(homeDir, '.ssh', keyPath);
919
- }
920
- }
921
- // Avoid duplicates if user config points to standard keys
922
- if (!candidatePaths.includes(resolvedKeyPath)) {
923
- candidatePaths.unshift(resolvedKeyPath); // Priority to config keys
924
- }
925
- }
926
- }
927
- const keys = [];
928
- for (const candidate of candidatePaths) {
857
+ }
858
+ resolveIdentityPath(identityPath) {
859
+ const trimmed = identityPath.trim();
860
+ if (!trimmed) {
861
+ return trimmed;
862
+ }
863
+ if (trimmed.startsWith("~")) {
864
+ const homeDir = os.homedir();
865
+ return path.resolve(homeDir, trimmed.slice(1));
866
+ }
867
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
868
+ }
869
+ loadDefaultIdentityFiles(host) {
870
+ const homeDir = os.homedir();
871
+ const candidatePaths = [
872
+ path.join(homeDir, ".ssh", "id_ed25519"),
873
+ path.join(homeDir, ".ssh", "id_rsa"),
874
+ path.join(homeDir, ".ssh", "id_ecdsa"),
875
+ path.join(homeDir, ".ssh", "id_dsa")
876
+ ];
877
+ if (host) {
878
+ const configKeys = this.getIdentityFilesFromConfig(host);
879
+ quickLog(`[SSH Config] Found ${configKeys.length} keys in config for host ${host}
880
+ `);
881
+ for (const keyPath of configKeys) {
882
+ let resolvedKeyPath = keyPath;
883
+ if (!path.isAbsolute(keyPath)) {
884
+ if (keyPath.startsWith("~")) {
885
+ resolvedKeyPath = keyPath.replace(/^~/, homeDir);
886
+ } else {
887
+ resolvedKeyPath = path.join(homeDir, ".ssh", keyPath);
888
+ }
889
+ }
890
+ if (!candidatePaths.includes(resolvedKeyPath)) {
891
+ candidatePaths.unshift(resolvedKeyPath);
892
+ }
893
+ }
894
+ }
895
+ const keys = [];
896
+ for (const candidate of candidatePaths) {
897
+ try {
898
+ if (fs.existsSync(candidate)) {
899
+ quickLog(`[SSH Auth] Loading key from: ${candidate}
900
+ `);
901
+ keys.push(fs.readFileSync(candidate));
902
+ }
903
+ } catch {
904
+ }
905
+ }
906
+ return keys;
907
+ }
908
+ /**
909
+ * Parse ~/.ssh/config to find IdentityFile entries for the given host.
910
+ * Supports standard SSH config patterns including wildcards (*, ?) and quoted paths.
911
+ */
912
+ getIdentityFilesFromConfig(targetHost) {
913
+ const homeDir = os.homedir();
914
+ const configPath = path.join(homeDir, ".ssh", "config");
915
+ const identityFiles = [];
916
+ if (!fs.existsSync(configPath)) {
917
+ return identityFiles;
918
+ }
919
+ try {
920
+ const content = fs.readFileSync(configPath, "utf8");
921
+ const lines = content.split("\n");
922
+ let inMatchingHost = false;
923
+ for (const line of lines) {
924
+ const trimmed = line.trim();
925
+ if (!trimmed || trimmed.startsWith("#")) continue;
926
+ const parts = trimmed.split(/\s+/);
927
+ const key = parts[0].toLowerCase();
928
+ if (key === "host") {
929
+ const patterns = parts.slice(1);
930
+ inMatchingHost = patterns.some((pattern) => {
929
931
  try {
930
- if (fs.existsSync(candidate)) {
931
- quickLog(`[SSH Auth] Loading key from: ${candidate}\n`);
932
- keys.push(fs.readFileSync(candidate));
933
- }
932
+ const regex = this.convertGlobToRegex(pattern);
933
+ return regex.test(targetHost);
934
+ } catch {
935
+ return false;
934
936
  }
935
- catch {
936
- // Ignore unreadable key files and keep trying.
937
+ });
938
+ } else if (inMatchingHost && key === "identityfile") {
939
+ const keyMatch = trimmed.match(/^\S+/);
940
+ if (keyMatch) {
941
+ let fileValue = trimmed.substring(keyMatch[0].length).trim();
942
+ if (fileValue.startsWith('"') && fileValue.endsWith('"') || fileValue.startsWith("'") && fileValue.endsWith("'")) {
943
+ fileValue = fileValue.slice(1, -1);
937
944
  }
938
- }
939
- return keys;
940
- }
941
- /**
942
- * Parse ~/.ssh/config to find IdentityFile entries for the given host.
943
- * Supports standard SSH config patterns including wildcards (*, ?) and quoted paths.
944
- */
945
- getIdentityFilesFromConfig(targetHost) {
946
- const homeDir = os.homedir();
947
- const configPath = path.join(homeDir, '.ssh', 'config');
948
- const identityFiles = [];
949
- if (!fs.existsSync(configPath)) {
950
- return identityFiles;
951
- }
952
- try {
953
- const content = fs.readFileSync(configPath, 'utf8');
954
- const lines = content.split('\n');
955
- let inMatchingHost = false;
956
- for (const line of lines) {
957
- const trimmed = line.trim();
958
- if (!trimmed || trimmed.startsWith('#'))
959
- continue;
960
- // Split by whitespace to get key and arguments
961
- const parts = trimmed.split(/\s+/);
962
- const key = parts[0].toLowerCase();
963
- if (key === 'host') {
964
- // Check if this Host block matches our target
965
- // Patterns can be separated by whitespace
966
- const patterns = parts.slice(1);
967
- inMatchingHost = patterns.some(pattern => {
968
- // Convert SSH glob pattern to regex
969
- try {
970
- const regex = this.convertGlobToRegex(pattern);
971
- return regex.test(targetHost);
972
- }
973
- catch {
974
- return false;
975
- }
976
- });
977
- }
978
- else if (inMatchingHost && key === 'identityfile') {
979
- // Found an IdentityFile for a matching host.
980
- // Extract the value robustly (handling potential quotes and spaces)
981
- // Find where the key ends and value begins in the original line
982
- const keyMatch = trimmed.match(/^\S+/);
983
- if (keyMatch) {
984
- let fileValue = trimmed.substring(keyMatch[0].length).trim();
985
- // Remove surrounding quotes if present
986
- if ((fileValue.startsWith('"') && fileValue.endsWith('"')) ||
987
- (fileValue.startsWith("'") && fileValue.endsWith("'"))) {
988
- fileValue = fileValue.slice(1, -1);
989
- }
990
- if (fileValue) {
991
- identityFiles.push(fileValue);
992
- }
993
- }
994
- }
945
+ if (fileValue) {
946
+ identityFiles.push(fileValue);
995
947
  }
948
+ }
996
949
  }
997
- catch (e) {
998
- quickLog(`[SSH Config] Failed to parse config file: ${e}\n`);
999
- }
1000
- return identityFiles;
950
+ }
951
+ } catch (e) {
952
+ quickLog(`[SSH Config] Failed to parse config file: ${e}
953
+ `);
1001
954
  }
1002
- /**
1003
- * Convert standard SSH config glob pattern to RegExp.
1004
- * Supports * (wildcard) and ? (single char).
1005
- */
1006
- convertGlobToRegex(pattern) {
1007
- // Escape special regex characters
1008
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
1009
- // Convert * to .* and ? to .
1010
- const regexString = '^' + escaped.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
1011
- return new RegExp(regexString);
1012
- }
1013
- /**
1014
- * Initialize SFTP session
1015
- */
1016
- async initializeSFTP() {
1017
- if (!this._client) {
1018
- throw new Error('SSH client not connected');
1019
- }
1020
- return new Promise((resolve, reject) => {
1021
- this._client.sftp((err, sftp) => {
1022
- if (err) {
1023
- reject(err);
1024
- }
1025
- else {
1026
- this.sftpClient = sftp;
1027
- resolve();
1028
- }
1029
- });
1030
- });
955
+ return identityFiles;
956
+ }
957
+ /**
958
+ * Convert standard SSH config glob pattern to RegExp.
959
+ * Supports * (wildcard) and ? (single char).
960
+ */
961
+ convertGlobToRegex(pattern) {
962
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
963
+ const regexString = "^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
964
+ return new RegExp(regexString);
965
+ }
966
+ /**
967
+ * Initialize SFTP session
968
+ */
969
+ async initializeSFTP() {
970
+ if (!this._client) {
971
+ throw new Error("SSH client not connected");
1031
972
  }
1032
- /**
1033
- * Detect the shell type on the remote system
1034
- */
1035
- async detectShellType() {
1036
- try {
1037
- const result = await this.executeCommand('echo $SHELL');
1038
- const shellPath = result.stdout.trim();
1039
- if (shellPath.includes('bash'))
1040
- return 'bash';
1041
- if (shellPath.includes('zsh'))
1042
- return 'zsh';
1043
- if (shellPath.includes('fish'))
1044
- return 'fish';
1045
- return 'bash'; // Default to bash
1046
- }
1047
- catch {
1048
- return 'bash';
1049
- }
973
+ return new Promise((resolve, reject) => {
974
+ this._client.sftp((err, sftp) => {
975
+ if (err) {
976
+ reject(err);
977
+ } else {
978
+ this.sftpClient = sftp;
979
+ resolve();
980
+ }
981
+ });
982
+ });
983
+ }
984
+ /**
985
+ * Detect the shell type on the remote system
986
+ */
987
+ async detectShellType() {
988
+ try {
989
+ const result = await this.executeCommand("echo $SHELL");
990
+ const shellPath = result.stdout.trim();
991
+ if (shellPath.includes("bash")) return "bash";
992
+ if (shellPath.includes("zsh")) return "zsh";
993
+ if (shellPath.includes("fish")) return "fish";
994
+ return "bash";
995
+ } catch {
996
+ return "bash";
1050
997
  }
1051
- /**
1052
- * Detect the operating system type
1053
- */
1054
- async detectOSType() {
1055
- try {
1056
- const result = await this.executeCommand('uname -s');
1057
- const osName = result.stdout.trim().toLowerCase();
1058
- if (osName.includes('darwin'))
1059
- return 'macos';
1060
- if (osName.includes('linux'))
1061
- return 'linux';
1062
- return 'linux'; // Default to linux
1063
- }
1064
- catch {
1065
- return 'linux';
1066
- }
998
+ }
999
+ /**
1000
+ * Detect the operating system type
1001
+ */
1002
+ async detectOSType() {
1003
+ try {
1004
+ const result = await this.executeCommand("uname -s");
1005
+ const osName = result.stdout.trim().toLowerCase();
1006
+ if (osName.includes("darwin")) return "macos";
1007
+ if (osName.includes("linux")) return "linux";
1008
+ return "linux";
1009
+ } catch {
1010
+ return "linux";
1067
1011
  }
1068
- /**
1069
- * Inject shell integration script
1070
- */
1071
- async injectShellIntegration() {
1072
- try {
1073
- const script = this.getShellIntegrationScript();
1074
- await this.executeCommand(script);
1075
- }
1076
- catch (error) {
1077
- // Shell integration is optional, don't fail if it doesn't work
1078
- console.warn('Failed to inject shell integration:', error);
1079
- }
1012
+ }
1013
+ /**
1014
+ * Inject shell integration script
1015
+ */
1016
+ async injectShellIntegration() {
1017
+ try {
1018
+ const script = this.getShellIntegrationScript();
1019
+ await this.executeCommand(script);
1020
+ } catch (error) {
1021
+ console.warn("Failed to inject shell integration:", error);
1080
1022
  }
1081
- /**
1082
- * Get shell integration script based on shell type
1083
- */
1084
- getShellIntegrationScript() {
1085
- const sessionId = this.sessionId;
1086
- if (this.shellType === 'bash') {
1087
- return `
1088
- export CENTAURUS_SUBSHELL=1
1089
- export CENTAURUS_SESSION_ID="${sessionId}"
1090
- _centaurus_pwd_hook() {
1091
- echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
1092
- }
1093
- export PROMPT_COMMAND="_centaurus_pwd_hook; \${PROMPT_COMMAND}"
1023
+ }
1024
+ /**
1025
+ * Get shell integration script based on shell type
1026
+ */
1027
+ getShellIntegrationScript() {
1028
+ const sessionId = this.sessionId;
1029
+ if (this.shellType === "bash") {
1030
+ return `
1031
+ export CENTAURUS_SUBSHELL=1
1032
+ export CENTAURUS_SESSION_ID="${sessionId}"
1033
+ _centaurus_pwd_hook() {
1034
+ echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
1035
+ }
1036
+ export PROMPT_COMMAND="_centaurus_pwd_hook; \${PROMPT_COMMAND}"
1094
1037
  `.trim();
1095
- }
1096
- else if (this.shellType === 'zsh') {
1097
- return `
1098
- export CENTAURUS_SUBSHELL=1
1099
- export CENTAURUS_SESSION_ID="${sessionId}"
1100
- _centaurus_pwd_hook() {
1101
- echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
1102
- }
1103
- precmd_functions+=(_centaurus_pwd_hook)
1038
+ } else if (this.shellType === "zsh") {
1039
+ return `
1040
+ export CENTAURUS_SUBSHELL=1
1041
+ export CENTAURUS_SESSION_ID="${sessionId}"
1042
+ _centaurus_pwd_hook() {
1043
+ echo "__CENTAURUS_PWD_${sessionId}__:$(pwd)"
1044
+ }
1045
+ precmd_functions+=(_centaurus_pwd_hook)
1104
1046
  `.trim();
1105
- }
1106
- else if (this.shellType === 'fish') {
1107
- return `
1108
- set -x CENTAURUS_SUBSHELL 1
1109
- set -x CENTAURUS_SESSION_ID "${sessionId}"
1110
- function _centaurus_pwd_hook --on-event fish_prompt
1111
- echo "__CENTAURUS_PWD_${sessionId}__:"(pwd)
1112
- end
1047
+ } else if (this.shellType === "fish") {
1048
+ return `
1049
+ set -x CENTAURUS_SUBSHELL 1
1050
+ set -x CENTAURUS_SESSION_ID "${sessionId}"
1051
+ function _centaurus_pwd_hook --on-event fish_prompt
1052
+ echo "__CENTAURUS_PWD_${sessionId}__:"(pwd)
1053
+ end
1113
1054
  `.trim();
1114
- }
1115
- // Default POSIX-compatible script
1116
- return `
1117
- export CENTAURUS_SUBSHELL=1
1118
- export CENTAURUS_SESSION_ID="${sessionId}"
1055
+ }
1056
+ return `
1057
+ export CENTAURUS_SUBSHELL=1
1058
+ export CENTAURUS_SESSION_ID="${sessionId}"
1119
1059
  `.trim();
1060
+ }
1061
+ /**
1062
+ * Resolve a path to an absolute path
1063
+ */
1064
+ resolveAbsolutePath(path2) {
1065
+ if (path2.startsWith("/")) {
1066
+ return path2;
1120
1067
  }
1121
- /**
1122
- * Resolve a path to an absolute path
1123
- */
1124
- resolveAbsolutePath(path) {
1125
- if (path.startsWith('/')) {
1126
- return path;
1127
- }
1128
- if (path.startsWith('~')) {
1129
- return path;
1130
- }
1131
- return `${this.currentWorkingDirectory}/${path}`;
1068
+ if (path2.startsWith("~")) {
1069
+ return path2;
1132
1070
  }
1133
- /**
1134
- * Parse directory listing output (ls -la)
1135
- */
1136
- parseDirectoryListing(output) {
1137
- const entries = [];
1138
- const lines = output.split('\n').filter(line => line.trim());
1139
- for (const line of lines) {
1140
- // Skip total line and . .. entries
1141
- if (line.startsWith('total') || line.endsWith(' .') || line.endsWith(' ..'))
1142
- continue;
1143
- const match = line.match(/^([drwx-]+)\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\S+\s+\S+\s+\S+\s+(.+)$/);
1144
- if (match) {
1145
- const permissions = match[1];
1146
- const size = parseInt(match[2], 10);
1147
- const name = match[3];
1148
- entries.push({
1149
- name,
1150
- type: permissions.startsWith('d') ? 'directory' : 'file',
1151
- size,
1152
- permissions,
1153
- });
1154
- }
1155
- }
1156
- return entries;
1071
+ return `${this.currentWorkingDirectory}/${path2}`;
1072
+ }
1073
+ /**
1074
+ * Parse directory listing output (ls -la)
1075
+ */
1076
+ parseDirectoryListing(output) {
1077
+ const entries = [];
1078
+ const lines = output.split("\n").filter((line) => line.trim());
1079
+ for (const line of lines) {
1080
+ if (line.startsWith("total") || line.endsWith(" .") || line.endsWith(" ..")) continue;
1081
+ const match = line.match(/^([drwx-]+)\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\S+\s+\S+\s+\S+\s+(.+)$/);
1082
+ if (match) {
1083
+ const permissions = match[1];
1084
+ const size = parseInt(match[2], 10);
1085
+ const name = match[3];
1086
+ entries.push({
1087
+ name,
1088
+ type: permissions.startsWith("d") ? "directory" : "file",
1089
+ size,
1090
+ permissions
1091
+ });
1092
+ }
1157
1093
  }
1158
- /**
1159
- * Create a new instance of this handler
1160
- */
1161
- createNew() {
1162
- const newHandler = new SSHHandler();
1163
- if (this.onPasswordRequest) {
1164
- newHandler.setPasswordRequestCallback(this.onPasswordRequest);
1165
- }
1166
- if (this.onDisconnectCallback) {
1167
- newHandler.setDisconnectCallback(this.onDisconnectCallback);
1168
- }
1169
- return newHandler;
1094
+ return entries;
1095
+ }
1096
+ /**
1097
+ * Create a new instance of this handler
1098
+ */
1099
+ createNew() {
1100
+ const newHandler = new SSHHandler();
1101
+ if (this.onPasswordRequest) {
1102
+ newHandler.setPasswordRequestCallback(this.onPasswordRequest);
1103
+ }
1104
+ if (this.onDisconnectCallback) {
1105
+ newHandler.setDisconnectCallback(this.onDisconnectCallback);
1170
1106
  }
1107
+ return newHandler;
1108
+ }
1171
1109
  }
1110
+ export {
1111
+ SSHHandler
1112
+ };
1172
1113
  //# sourceMappingURL=ssh-handler.js.map