@oh-my-pi/pi-coding-agent 0.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Shell environment snapshot for preserving user aliases, functions, and options.
3
+ *
4
+ * Creates a snapshot file that captures the user's shell environment from their
5
+ * .bashrc/.zshrc, which can be sourced before each command to provide a familiar
6
+ * shell experience.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ let cachedSnapshotPath: string | null = null;
14
+ let cleanupRegistered = false;
15
+
16
+ /**
17
+ * Get the user's shell config file path.
18
+ */
19
+ function getShellConfigFile(shell: string): string {
20
+ const home = homedir();
21
+ if (shell.includes("zsh")) return join(home, ".zshrc");
22
+ if (shell.includes("bash")) return join(home, ".bashrc");
23
+ return join(home, ".profile");
24
+ }
25
+
26
+ /**
27
+ * Generate the snapshot creation script.
28
+ * This script sources the user's rc file and extracts functions, aliases, and options.
29
+ * Matches Claude Code's snapshot generation logic.
30
+ */
31
+ function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
32
+ const hasRcFile = existsSync(rcFile);
33
+ const isZsh = shell.includes("zsh");
34
+
35
+ // Escape the snapshot path for shell
36
+ const escapedPath = snapshotPath.replace(/'/g, "'\\''");
37
+
38
+ // Function extraction differs between bash and zsh
39
+ const functionScript = isZsh
40
+ ? `
41
+ echo "# Functions" >> "$SNAPSHOT_FILE"
42
+ # Force autoload all functions first
43
+ typeset -f > /dev/null 2>&1
44
+ # Get user function names - filter system/private ones
45
+ typeset +f 2>/dev/null | grep -vE '^(_|__)' | while read func; do
46
+ typeset -f "$func" >> "$SNAPSHOT_FILE" 2>/dev/null
47
+ done
48
+ `
49
+ : `
50
+ echo "# Functions" >> "$SNAPSHOT_FILE"
51
+ # Force autoload all functions first
52
+ declare -f > /dev/null 2>&1
53
+ # Get user function names - filter system/private ones, use base64 for special chars
54
+ declare -F 2>/dev/null | cut -d' ' -f3 | grep -vE '^(_|__)' | while read func; do
55
+ encoded_func=$(declare -f "$func" | base64)
56
+ echo "eval \\"\\$(echo '$encoded_func' | base64 -d)\\" > /dev/null 2>&1" >> "$SNAPSHOT_FILE"
57
+ done
58
+ `;
59
+
60
+ // Shell options extraction
61
+ const optionsScript = isZsh
62
+ ? `
63
+ echo "# Shell Options" >> "$SNAPSHOT_FILE"
64
+ setopt 2>/dev/null | sed 's/^/setopt /' | head -n 1000 >> "$SNAPSHOT_FILE"
65
+ `
66
+ : `
67
+ echo "# Shell Options" >> "$SNAPSHOT_FILE"
68
+ shopt -p 2>/dev/null | head -n 1000 >> "$SNAPSHOT_FILE"
69
+ set -o 2>/dev/null | grep "on" | awk '{print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
70
+ echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"
71
+ `;
72
+
73
+ return `
74
+ SNAPSHOT_FILE='${escapedPath}'
75
+
76
+ # Source user's rc file if it exists
77
+ ${hasRcFile ? `source "${rcFile}" < /dev/null 2>/dev/null` : "# No user config file to source"}
78
+
79
+ # Create/clear the snapshot file
80
+ echo "# Shell snapshot - generated by omp agent" >| "$SNAPSHOT_FILE"
81
+
82
+ # Unalias everything first to avoid conflicts when sourced
83
+ echo "unalias -a 2>/dev/null || true" >> "$SNAPSHOT_FILE"
84
+
85
+ ${functionScript}
86
+
87
+ ${optionsScript}
88
+
89
+ # Export aliases (limit to 1000)
90
+ echo "# Aliases" >> "$SNAPSHOT_FILE"
91
+ # Filter out winpty aliases on Windows to avoid "stdin is not a tty" errors
92
+ if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
93
+ alias 2>/dev/null | grep -v "='winpty " | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
94
+ else
95
+ alias 2>/dev/null | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
96
+ fi
97
+
98
+ # Export PATH
99
+ echo "export PATH='$PATH'" >> "$SNAPSHOT_FILE"
100
+
101
+ # Verify snapshot was created
102
+ if [ ! -f "$SNAPSHOT_FILE" ]; then
103
+ echo "Error: Snapshot file was not created" >&2
104
+ exit 1
105
+ fi
106
+ `.trim();
107
+ }
108
+
109
+ /**
110
+ * Create a shell snapshot, caching the result.
111
+ * Returns the path to the snapshot file, or null if creation failed.
112
+ */
113
+ export async function getOrCreateSnapshot(
114
+ shell: string,
115
+ env: Record<string, string | undefined>,
116
+ ): Promise<string | null> {
117
+ // Return cached snapshot if valid
118
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
119
+ return cachedSnapshotPath;
120
+ }
121
+
122
+ // Skip on Windows (no .bashrc in standard location)
123
+ if (process.platform === "win32") {
124
+ return null;
125
+ }
126
+
127
+ const rcFile = getShellConfigFile(shell);
128
+
129
+ // Create snapshot directory
130
+ const snapshotDir = join(tmpdir(), "omp-shell-snapshots");
131
+ try {
132
+ mkdirSync(snapshotDir, { recursive: true });
133
+ } catch {
134
+ return null;
135
+ }
136
+
137
+ // Generate unique snapshot path
138
+ const timestamp = Date.now();
139
+ const random = Math.random().toString(36).substring(2, 8);
140
+ const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
141
+ const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${timestamp}-${random}.sh`);
142
+
143
+ // Generate and execute snapshot script
144
+ const script = generateSnapshotScript(shell, snapshotPath, rcFile);
145
+
146
+ try {
147
+ const result = Bun.spawnSync([shell, "-l", "-c", script], {
148
+ stdin: "ignore",
149
+ stdout: "pipe",
150
+ stderr: "pipe",
151
+ env,
152
+ timeout: 10000, // 10 second timeout
153
+ });
154
+
155
+ if (result.exitCode === 0 && existsSync(snapshotPath)) {
156
+ cachedSnapshotPath = snapshotPath;
157
+ registerCleanup();
158
+ return snapshotPath;
159
+ }
160
+ } catch {
161
+ // Snapshot creation failed, proceed without it
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Get the command prefix to source the snapshot.
169
+ * Returns empty string if no snapshot available.
170
+ */
171
+ export function getSnapshotSourceCommand(snapshotPath: string | null): string {
172
+ if (!snapshotPath) return "";
173
+ // Escape for shell
174
+ const escaped = snapshotPath.replace(/'/g, "'\\''");
175
+ return `source '${escaped}' 2>/dev/null && `;
176
+ }
177
+
178
+ /**
179
+ * Register cleanup handler to delete snapshot on process exit.
180
+ */
181
+ function registerCleanup(): void {
182
+ if (cleanupRegistered) return;
183
+ cleanupRegistered = true;
184
+
185
+ const cleanup = () => {
186
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
187
+ try {
188
+ unlinkSync(cachedSnapshotPath);
189
+ } catch {
190
+ // Ignore cleanup errors
191
+ }
192
+ }
193
+ };
194
+
195
+ process.on("exit", cleanup);
196
+ process.on("SIGINT", () => {
197
+ cleanup();
198
+ process.exit(130);
199
+ });
200
+ process.on("SIGTERM", () => {
201
+ cleanup();
202
+ process.exit(143);
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Clear the cached snapshot (for testing or forced refresh).
208
+ */
209
+ export function clearSnapshotCache(): void {
210
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
211
+ try {
212
+ unlinkSync(cachedSnapshotPath);
213
+ } catch {
214
+ // Ignore
215
+ }
216
+ }
217
+ cachedSnapshotPath = null;
218
+ }
@@ -0,0 +1,364 @@
1
+ import { accessSync, constants, existsSync } from "node:fs";
2
+ import { SettingsManager } from "../core/settings-manager";
3
+
4
+ export interface ShellConfig {
5
+ shell: string;
6
+ args: string[];
7
+ env: Record<string, string | undefined>;
8
+ prefix: string | undefined;
9
+ }
10
+
11
+ let cachedShellConfig: ShellConfig | null = null;
12
+
13
+ /**
14
+ * Check if a shell binary is executable.
15
+ */
16
+ function isExecutable(path: string): boolean {
17
+ try {
18
+ accessSync(path, constants.X_OK);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Build the spawn environment (cached).
27
+ */
28
+ function buildSpawnEnv(shell: string): Record<string, string | undefined> {
29
+ const noCI = process.env.OMP_BASH_NO_CI || process.env.CLAUDE_BASH_NO_CI;
30
+ return {
31
+ ...process.env,
32
+ SHELL: shell,
33
+ GIT_EDITOR: "true",
34
+ GPG_TTY: "not a tty",
35
+ OMPCODE: "1",
36
+ CLAUDECODE: "1",
37
+ ...(noCI ? {} : { CI: "true" }),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Get shell args, optionally including login shell flag.
43
+ * Supports OMP_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l.
44
+ */
45
+ function getShellArgs(): string[] {
46
+ const noLogin = process.env.OMP_BASH_NO_LOGIN || process.env.CLAUDE_BASH_NO_LOGIN;
47
+ return noLogin ? ["-c"] : ["-l", "-c"];
48
+ }
49
+
50
+ /**
51
+ * Get shell prefix for wrapping commands (profilers, strace, etc.).
52
+ */
53
+ function getShellPrefix(): string | undefined {
54
+ return process.env.OMP_SHELL_PREFIX || process.env.CLAUDE_CODE_SHELL_PREFIX;
55
+ }
56
+
57
+ /**
58
+ * Find bash executable on PATH (Windows)
59
+ */
60
+ function findBashOnPath(): string | null {
61
+ try {
62
+ const result = Bun.spawnSync(["where", "bash.exe"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
63
+ if (result.exitCode === 0 && result.stdout) {
64
+ const firstMatch = result.stdout.toString().trim().split(/\r?\n/)[0];
65
+ if (firstMatch && existsSync(firstMatch)) {
66
+ return firstMatch;
67
+ }
68
+ }
69
+ } catch {
70
+ // Ignore errors
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Build full shell config from a shell path.
77
+ */
78
+ function buildConfig(shell: string): ShellConfig {
79
+ return {
80
+ shell,
81
+ args: getShellArgs(),
82
+ env: buildSpawnEnv(shell),
83
+ prefix: getShellPrefix(),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Get shell configuration based on platform.
89
+ * Resolution order:
90
+ * 1. User-specified shellPath in settings.json
91
+ * 2. On Windows: Git Bash in known locations, then bash on PATH
92
+ * 3. On Unix: $SHELL if bash/zsh, then fallback paths
93
+ * 4. Fallback: sh
94
+ */
95
+ export function getShellConfig(): ShellConfig {
96
+ if (cachedShellConfig) {
97
+ return cachedShellConfig;
98
+ }
99
+
100
+ const settings = SettingsManager.create();
101
+ const customShellPath = settings.getShellPath();
102
+
103
+ // 1. Check user-specified shell path
104
+ if (customShellPath) {
105
+ if (existsSync(customShellPath)) {
106
+ cachedShellConfig = buildConfig(customShellPath);
107
+ return cachedShellConfig;
108
+ }
109
+ throw new Error(
110
+ `Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.omp/agent/settings.json`,
111
+ );
112
+ }
113
+
114
+ if (process.platform === "win32") {
115
+ // 2. Try Git Bash in known locations
116
+ const paths: string[] = [];
117
+ const programFiles = process.env.ProgramFiles;
118
+ if (programFiles) {
119
+ paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
120
+ }
121
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
122
+ if (programFilesX86) {
123
+ paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
124
+ }
125
+
126
+ for (const path of paths) {
127
+ if (existsSync(path)) {
128
+ cachedShellConfig = buildConfig(path);
129
+ return cachedShellConfig;
130
+ }
131
+ }
132
+
133
+ // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
134
+ const bashOnPath = findBashOnPath();
135
+ if (bashOnPath) {
136
+ cachedShellConfig = buildConfig(bashOnPath);
137
+ return cachedShellConfig;
138
+ }
139
+
140
+ throw new Error(
141
+ `No bash shell found. Options:\n` +
142
+ ` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
143
+ ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
144
+ ` 3. Set shellPath in ~/.omp/agent/settings.json\n\n` +
145
+ `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`,
146
+ );
147
+ }
148
+
149
+ // Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
150
+ const userShell = process.env.SHELL;
151
+ const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
152
+ if (isValidShell && isExecutable(userShell)) {
153
+ cachedShellConfig = buildConfig(userShell);
154
+ return cachedShellConfig;
155
+ }
156
+
157
+ // Fallback paths (Claude's approach: check known locations)
158
+ const fallbackPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
159
+ const preferZsh = !userShell?.includes("bash");
160
+ const shellOrder = preferZsh ? ["zsh", "bash"] : ["bash", "zsh"];
161
+
162
+ for (const shellName of shellOrder) {
163
+ for (const dir of fallbackPaths) {
164
+ const shellPath = `${dir}/${shellName}`;
165
+ if (isExecutable(shellPath)) {
166
+ cachedShellConfig = buildConfig(shellPath);
167
+ return cachedShellConfig;
168
+ }
169
+ }
170
+ }
171
+
172
+ // Last resort: use Bun.which
173
+ const bashPath = Bun.which("bash");
174
+ if (bashPath) {
175
+ cachedShellConfig = buildConfig(bashPath);
176
+ return cachedShellConfig;
177
+ }
178
+
179
+ const shPath = Bun.which("sh");
180
+ cachedShellConfig = buildConfig(shPath || "sh");
181
+ return cachedShellConfig;
182
+ }
183
+
184
+ /**
185
+ * Sanitize binary output for display/storage.
186
+ * Removes characters that crash string-width or cause display issues:
187
+ * - Control characters (except tab, newline, carriage return)
188
+ * - Lone surrogates
189
+ * - Unicode Format characters (crash string-width due to a bug)
190
+ * - Characters with undefined code points
191
+ */
192
+ export function sanitizeBinaryOutput(str: string): string {
193
+ // Use Array.from to properly iterate over code points (not code units)
194
+ // This handles surrogate pairs correctly and catches edge cases where
195
+ // codePointAt() might return undefined
196
+ return Array.from(str)
197
+ .filter((char) => {
198
+ // Filter out characters that cause string-width to crash
199
+ // This includes:
200
+ // - Unicode format characters
201
+ // - Lone surrogates (already filtered by Array.from)
202
+ // - Control chars except \t \n \r
203
+ // - Characters with undefined code points
204
+
205
+ const code = char.codePointAt(0);
206
+
207
+ // Skip if code point is undefined (edge case with invalid strings)
208
+ if (code === undefined) return false;
209
+
210
+ // Allow tab, newline, carriage return
211
+ if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
212
+
213
+ // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
214
+ if (code <= 0x1f) return false;
215
+
216
+ // Filter out Unicode format characters
217
+ if (code >= 0xfff9 && code <= 0xfffb) return false;
218
+
219
+ return true;
220
+ })
221
+ .join("");
222
+ }
223
+
224
+ let pgrepAvailable: boolean | null = null;
225
+
226
+ /**
227
+ * Check if pgrep is available on this system (cached).
228
+ */
229
+ function hasPgrep(): boolean {
230
+ if (pgrepAvailable === null) {
231
+ try {
232
+ const result = Bun.spawnSync(["pgrep", "--version"], {
233
+ stdin: "ignore",
234
+ stdout: "ignore",
235
+ stderr: "ignore",
236
+ });
237
+ // pgrep exists if it ran (exit 0 or 1 are both valid)
238
+ pgrepAvailable = result.exitCode !== null;
239
+ } catch {
240
+ pgrepAvailable = false;
241
+ }
242
+ }
243
+ return pgrepAvailable;
244
+ }
245
+
246
+ /**
247
+ * Get direct children of a PID using pgrep.
248
+ */
249
+ function getChildrenViaPgrep(pid: number): number[] {
250
+ const result = Bun.spawnSync(["pgrep", "-P", String(pid)], {
251
+ stdin: "ignore",
252
+ stdout: "pipe",
253
+ stderr: "ignore",
254
+ });
255
+
256
+ if (result.exitCode !== 0 || !result.stdout) return [];
257
+
258
+ const children: number[] = [];
259
+ for (const line of result.stdout.toString().trim().split("\n")) {
260
+ const childPid = parseInt(line, 10);
261
+ if (!Number.isNaN(childPid)) children.push(childPid);
262
+ }
263
+ return children;
264
+ }
265
+
266
+ /**
267
+ * Get direct children of a PID using /proc (Linux only).
268
+ */
269
+ function getChildrenViaProc(pid: number): number[] {
270
+ try {
271
+ const result = Bun.spawnSync(
272
+ [
273
+ "sh",
274
+ "-c",
275
+ `for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`,
276
+ ],
277
+ { stdin: "ignore", stdout: "pipe", stderr: "ignore" },
278
+ );
279
+ if (result.exitCode !== 0 || !result.stdout) return [];
280
+
281
+ const children: number[] = [];
282
+ for (const line of result.stdout.toString().trim().split("\n")) {
283
+ const childPid = parseInt(line, 10);
284
+ if (!Number.isNaN(childPid)) children.push(childPid);
285
+ }
286
+ return children;
287
+ } catch {
288
+ return [];
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Collect all descendant PIDs breadth-first.
294
+ * Returns deepest descendants first (reverse BFS order) for proper kill ordering.
295
+ */
296
+ function getDescendantPids(pid: number): number[] {
297
+ const getChildren = hasPgrep() ? getChildrenViaPgrep : getChildrenViaProc;
298
+ const descendants: number[] = [];
299
+ const queue = [pid];
300
+
301
+ while (queue.length > 0) {
302
+ const current = queue.shift()!;
303
+ const children = getChildren(current);
304
+ for (const child of children) {
305
+ descendants.push(child);
306
+ queue.push(child);
307
+ }
308
+ }
309
+
310
+ // Reverse so deepest children are killed first
311
+ return descendants.reverse();
312
+ }
313
+
314
+ function tryKill(pid: number, signal: NodeJS.Signals): boolean {
315
+ try {
316
+ process.kill(pid, signal);
317
+ return true;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Kill a process and all its descendants.
325
+ * @param gracePeriodMs - Time to wait after SIGTERM before SIGKILL (0 = immediate SIGKILL)
326
+ */
327
+ export function killProcessTree(pid: number, gracePeriodMs = 0): void {
328
+ if (process.platform === "win32") {
329
+ Bun.spawnSync(["taskkill", "/F", "/T", "/PID", String(pid)], {
330
+ stdin: "ignore",
331
+ stdout: "ignore",
332
+ stderr: "ignore",
333
+ });
334
+ return;
335
+ }
336
+
337
+ const signal = gracePeriodMs > 0 ? "SIGTERM" : "SIGKILL";
338
+
339
+ // Fast path: process group kill (works if pid is group leader)
340
+ try {
341
+ process.kill(-pid, signal);
342
+ if (gracePeriodMs > 0) {
343
+ Bun.sleepSync(gracePeriodMs);
344
+ try {
345
+ process.kill(-pid, "SIGKILL");
346
+ } catch {
347
+ // Already dead
348
+ }
349
+ }
350
+ return;
351
+ } catch {
352
+ // Not a process group leader, fall through
353
+ }
354
+
355
+ // Collect descendants BEFORE killing to minimize race window
356
+ const allPids = [...getDescendantPids(pid), pid];
357
+
358
+ if (gracePeriodMs > 0) {
359
+ for (const p of allPids) tryKill(p, "SIGTERM");
360
+ Bun.sleepSync(gracePeriodMs);
361
+ }
362
+
363
+ for (const p of allPids) tryKill(p, "SIGKILL");
364
+ }