@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,258 @@
1
+ /**
2
+ * Credential storage for API keys and OAuth tokens.
3
+ * Handles loading, saving, and refreshing credentials from auth.json.
4
+ */
5
+
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import {
9
+ getEnvApiKey,
10
+ getOAuthApiKey,
11
+ loginAnthropic,
12
+ loginAntigravity,
13
+ loginGeminiCli,
14
+ loginGitHubCopilot,
15
+ type OAuthCredentials,
16
+ type OAuthProvider,
17
+ } from "@oh-my-pi/pi-ai";
18
+ import { logger } from "./logger";
19
+
20
+ export type ApiKeyCredential = {
21
+ type: "api_key";
22
+ key: string;
23
+ };
24
+
25
+ export type OAuthCredential = {
26
+ type: "oauth";
27
+ } & OAuthCredentials;
28
+
29
+ export type AuthCredential = ApiKeyCredential | OAuthCredential;
30
+
31
+ export type AuthStorageData = Record<string, AuthCredential>;
32
+
33
+ /**
34
+ * Credential storage backed by a JSON file.
35
+ * Reads from multiple fallback paths, writes to primary path.
36
+ */
37
+ export class AuthStorage {
38
+ private data: AuthStorageData = {};
39
+ private runtimeOverrides: Map<string, string> = new Map();
40
+ private fallbackResolver?: (provider: string) => string | undefined;
41
+
42
+ /**
43
+ * @param authPath - Primary path for reading/writing auth.json
44
+ * @param fallbackPaths - Additional paths to check when reading (legacy support)
45
+ */
46
+ constructor(
47
+ private authPath: string,
48
+ private fallbackPaths: string[] = [],
49
+ ) {
50
+ this.reload();
51
+ }
52
+
53
+ /**
54
+ * Set a runtime API key override (not persisted to disk).
55
+ * Used for CLI --api-key flag.
56
+ */
57
+ setRuntimeApiKey(provider: string, apiKey: string): void {
58
+ this.runtimeOverrides.set(provider, apiKey);
59
+ }
60
+
61
+ /**
62
+ * Remove a runtime API key override.
63
+ */
64
+ removeRuntimeApiKey(provider: string): void {
65
+ this.runtimeOverrides.delete(provider);
66
+ }
67
+
68
+ /**
69
+ * Set a fallback resolver for API keys not found in auth.json or env vars.
70
+ * Used for custom provider keys from models.json.
71
+ */
72
+ setFallbackResolver(resolver: (provider: string) => string | undefined): void {
73
+ this.fallbackResolver = resolver;
74
+ }
75
+
76
+ /**
77
+ * Reload credentials from disk.
78
+ * Checks primary path first, then fallback paths.
79
+ */
80
+ reload(): void {
81
+ const pathsToCheck = [this.authPath, ...this.fallbackPaths];
82
+
83
+ logger.debug("AuthStorage.reload checking paths", { paths: pathsToCheck });
84
+
85
+ for (const authPath of pathsToCheck) {
86
+ const exists = existsSync(authPath);
87
+ logger.debug("AuthStorage.reload path check", { path: authPath, exists });
88
+
89
+ if (exists) {
90
+ try {
91
+ this.data = JSON.parse(readFileSync(authPath, "utf-8"));
92
+ logger.debug("AuthStorage.reload loaded", { path: authPath, providers: Object.keys(this.data) });
93
+ return;
94
+ } catch (e) {
95
+ logger.error("AuthStorage failed to parse auth file", { path: authPath, error: String(e) });
96
+ // Continue to next path on parse error
97
+ }
98
+ }
99
+ }
100
+
101
+ logger.warn("AuthStorage no auth file found", { checkedPaths: pathsToCheck });
102
+ this.data = {};
103
+ }
104
+
105
+ /**
106
+ * Save credentials to disk.
107
+ */
108
+ private save(): void {
109
+ const dir = dirname(this.authPath);
110
+ if (!existsSync(dir)) {
111
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
112
+ }
113
+ Bun.write(this.authPath, JSON.stringify(this.data, null, 2));
114
+ chmodSync(this.authPath, 0o600);
115
+ }
116
+
117
+ /**
118
+ * Get credential for a provider.
119
+ */
120
+ get(provider: string): AuthCredential | undefined {
121
+ return this.data[provider] ?? undefined;
122
+ }
123
+
124
+ /**
125
+ * Set credential for a provider.
126
+ */
127
+ set(provider: string, credential: AuthCredential): void {
128
+ this.data[provider] = credential;
129
+ this.save();
130
+ }
131
+
132
+ /**
133
+ * Remove credential for a provider.
134
+ */
135
+ remove(provider: string): void {
136
+ delete this.data[provider];
137
+ this.save();
138
+ }
139
+
140
+ /**
141
+ * List all providers with credentials.
142
+ */
143
+ list(): string[] {
144
+ return Object.keys(this.data);
145
+ }
146
+
147
+ /**
148
+ * Check if credentials exist for a provider.
149
+ */
150
+ has(provider: string): boolean {
151
+ return provider in this.data;
152
+ }
153
+
154
+ /**
155
+ * Get all credentials (for passing to getOAuthApiKey).
156
+ */
157
+ getAll(): AuthStorageData {
158
+ return { ...this.data };
159
+ }
160
+
161
+ /**
162
+ * Login to an OAuth provider.
163
+ */
164
+ async login(
165
+ provider: OAuthProvider,
166
+ callbacks: {
167
+ onAuth: (info: { url: string; instructions?: string }) => void;
168
+ onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
169
+ onProgress?: (message: string) => void;
170
+ },
171
+ ): Promise<void> {
172
+ let credentials: OAuthCredentials;
173
+
174
+ switch (provider) {
175
+ case "anthropic":
176
+ credentials = await loginAnthropic(
177
+ (url) => callbacks.onAuth({ url }),
178
+ () => callbacks.onPrompt({ message: "Paste the authorization code:" }),
179
+ );
180
+ break;
181
+ case "github-copilot":
182
+ credentials = await loginGitHubCopilot({
183
+ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
184
+ onPrompt: callbacks.onPrompt,
185
+ onProgress: callbacks.onProgress,
186
+ });
187
+ break;
188
+ case "google-gemini-cli":
189
+ credentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);
190
+ break;
191
+ case "google-antigravity":
192
+ credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);
193
+ break;
194
+ default:
195
+ throw new Error(`Unknown OAuth provider: ${provider}`);
196
+ }
197
+
198
+ this.set(provider, { type: "oauth", ...credentials });
199
+ }
200
+
201
+ /**
202
+ * Logout from a provider.
203
+ */
204
+ logout(provider: string): void {
205
+ this.remove(provider);
206
+ }
207
+
208
+ /**
209
+ * Get API key for a provider.
210
+ * Priority:
211
+ * 1. Runtime override (CLI --api-key)
212
+ * 2. API key from auth.json
213
+ * 3. OAuth token from auth.json (auto-refreshed)
214
+ * 4. Environment variable
215
+ * 5. Fallback resolver (models.json custom providers)
216
+ */
217
+ async getApiKey(provider: string): Promise<string | undefined> {
218
+ // Runtime override takes highest priority
219
+ const runtimeKey = this.runtimeOverrides.get(provider);
220
+ if (runtimeKey) {
221
+ return runtimeKey;
222
+ }
223
+
224
+ const cred = this.data[provider];
225
+
226
+ if (cred?.type === "api_key") {
227
+ return cred.key;
228
+ }
229
+
230
+ if (cred?.type === "oauth") {
231
+ // Filter to only oauth credentials for getOAuthApiKey
232
+ const oauthCreds: Record<string, OAuthCredentials> = {};
233
+ for (const [key, value] of Object.entries(this.data)) {
234
+ if (value.type === "oauth") {
235
+ oauthCreds[key] = value;
236
+ }
237
+ }
238
+
239
+ try {
240
+ const result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
241
+ if (result) {
242
+ this.data[provider] = { type: "oauth", ...result.newCredentials };
243
+ this.save();
244
+ return result.apiKey;
245
+ }
246
+ } catch {
247
+ this.remove(provider);
248
+ }
249
+ }
250
+
251
+ // Fall back to environment variable
252
+ const envKey = getEnvApiKey(provider);
253
+ if (envKey) return envKey;
254
+
255
+ // Fall back to custom resolver (e.g., models.json custom providers)
256
+ return this.fallbackResolver?.(provider) ?? undefined;
257
+ }
258
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Bash command execution with streaming support and cancellation.
3
+ *
4
+ * This module provides a unified bash execution implementation used by:
5
+ * - AgentSession.executeBash() for interactive and RPC modes
6
+ * - Direct calls from modes that need bash execution
7
+ */
8
+
9
+ import { createWriteStream, type WriteStream } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import type { Subprocess } from "bun";
13
+ import stripAnsi from "strip-ansi";
14
+ import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
15
+ import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
16
+ import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate";
17
+ import { ScopeSignal } from "./utils";
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface BashExecutorOptions {
24
+ /** Working directory for command execution */
25
+ cwd?: string;
26
+ /** Timeout in milliseconds */
27
+ timeout?: number;
28
+ /** Callback for streaming output chunks (already sanitized) */
29
+ onChunk?: (chunk: string) => void;
30
+ /** AbortSignal for cancellation */
31
+ signal?: AbortSignal;
32
+ }
33
+
34
+ export interface BashResult {
35
+ /** Combined stdout + stderr output (sanitized, possibly truncated) */
36
+ output: string;
37
+ /** Process exit code (undefined if killed/cancelled) */
38
+ exitCode: number | undefined;
39
+ /** Whether the command was cancelled via signal */
40
+ cancelled: boolean;
41
+ /** Whether the output was truncated */
42
+ truncated: boolean;
43
+ /** Path to temp file containing full output (if output exceeded truncation threshold) */
44
+ fullOutputPath?: string;
45
+ }
46
+
47
+ // ============================================================================
48
+ // Implementation
49
+ // ============================================================================
50
+
51
+ function createSanitizer(): TransformStream<Uint8Array, string> {
52
+ const decoder = new TextDecoder();
53
+ return new TransformStream({
54
+ transform(chunk, controller) {
55
+ const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(chunk, { stream: true }))).replace(/\r/g, "");
56
+ controller.enqueue(text);
57
+ },
58
+ });
59
+ }
60
+
61
+ function createOutputSink(
62
+ spillThreshold: number,
63
+ maxBuffer: number,
64
+ onChunk?: (text: string) => void,
65
+ ): WritableStream<string> & {
66
+ dump: (annotation?: string) => { output: string; truncated: boolean; fullOutputPath?: string };
67
+ } {
68
+ const chunks: string[] = [];
69
+ let chunkBytes = 0;
70
+ let totalBytes = 0;
71
+ let fullOutputPath: string | undefined;
72
+ let fullOutputStream: WriteStream | undefined;
73
+
74
+ const sink = new WritableStream<string>({
75
+ write(text) {
76
+ totalBytes += text.length;
77
+
78
+ // Spill to temp file if needed
79
+ if (totalBytes > spillThreshold && !fullOutputPath) {
80
+ fullOutputPath = join(tmpdir(), `omp-${crypto.randomUUID()}.buffer`);
81
+ const ts = createWriteStream(fullOutputPath);
82
+ chunks.forEach((c) => {
83
+ ts.write(c);
84
+ });
85
+ fullOutputStream = ts;
86
+ }
87
+ fullOutputStream?.write(text);
88
+
89
+ // Rolling buffer
90
+ chunks.push(text);
91
+ chunkBytes += text.length;
92
+ while (chunkBytes > maxBuffer && chunks.length > 1) {
93
+ chunkBytes -= chunks.shift()!.length;
94
+ }
95
+
96
+ onChunk?.(text);
97
+ },
98
+ close() {
99
+ fullOutputStream?.end();
100
+ },
101
+ });
102
+
103
+ return Object.assign(sink, {
104
+ dump(annotation?: string) {
105
+ if (annotation) {
106
+ chunks.push(`\n\n${annotation}`);
107
+ }
108
+ const full = chunks.join("");
109
+ const { content, truncated } = truncateTail(full);
110
+ return { output: truncated ? content : full, truncated, fullOutputPath: fullOutputPath };
111
+ },
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Execute a bash command with optional streaming and cancellation support.
117
+ *
118
+ * Features:
119
+ * - Streams sanitized output via onChunk callback
120
+ * - Writes large output to temp file for later retrieval
121
+ * - Supports cancellation via AbortSignal
122
+ * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)
123
+ * - Truncates output if it exceeds the default max bytes
124
+ *
125
+ * @param command - The bash command to execute
126
+ * @param options - Optional streaming callback and abort signal
127
+ * @returns Promise resolving to execution result
128
+ */
129
+ export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
130
+ const { shell, args, env, prefix } = getShellConfig();
131
+
132
+ // Get or create shell snapshot (for aliases, functions, options)
133
+ const snapshotPath = await getOrCreateSnapshot(shell, env);
134
+ const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
135
+
136
+ // Build final command: snapshot + prefix + command
137
+ const prefixedCommand = prefix ? `${prefix} ${command}` : command;
138
+ const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
139
+
140
+ using signal = new ScopeSignal(options);
141
+
142
+ const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
143
+ cwd: options?.cwd,
144
+ stdin: "ignore",
145
+ stdout: "pipe",
146
+ stderr: "pipe",
147
+ env,
148
+ });
149
+
150
+ signal.catch(() => {
151
+ killProcessTree(child.pid);
152
+ });
153
+
154
+ const sink = createOutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
155
+
156
+ const writer = sink.getWriter();
157
+ try {
158
+ async function pumpStream(readable: ReadableStream<Uint8Array>) {
159
+ const reader = readable.pipeThrough(createSanitizer()).getReader();
160
+ try {
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) break;
164
+ await writer.write(value);
165
+ }
166
+ } finally {
167
+ reader.releaseLock();
168
+ }
169
+ }
170
+ await Promise.all([
171
+ pumpStream(child.stdout as ReadableStream<Uint8Array>),
172
+ pumpStream(child.stderr as ReadableStream<Uint8Array>),
173
+ ]);
174
+ } finally {
175
+ await writer.close();
176
+ }
177
+
178
+ // Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
179
+ const exitCode = await child.exited;
180
+
181
+ const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
182
+
183
+ if (signal.timedOut()) {
184
+ const secs = Math.round(options!.timeout! / 1000);
185
+ return {
186
+ exitCode: undefined,
187
+ cancelled: true,
188
+ ...sink.dump(`Command timed out after ${secs} seconds`),
189
+ };
190
+ }
191
+
192
+ return {
193
+ exitCode: cancelled ? undefined : exitCode,
194
+ cancelled,
195
+ ...sink.dump(),
196
+ };
197
+ }