@mariozechner/pi-coding-agent 0.14.2 → 0.16.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 (316) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +415 -1098
  3. package/dist/cli/args.d.ts +30 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/args.js +179 -0
  6. package/dist/cli/args.js.map +1 -0
  7. package/dist/cli/file-processor.d.ts +11 -0
  8. package/dist/cli/file-processor.d.ts.map +1 -0
  9. package/dist/cli/file-processor.js +82 -0
  10. package/dist/cli/file-processor.js.map +1 -0
  11. package/dist/cli/session-picker.d.ts +7 -0
  12. package/dist/cli/session-picker.d.ts.map +1 -0
  13. package/dist/cli/session-picker.js +29 -0
  14. package/dist/cli/session-picker.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +7 -18
  17. package/dist/cli.js.map +1 -1
  18. package/dist/config.d.ts +2 -2
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +15 -9
  21. package/dist/config.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +287 -0
  23. package/dist/core/agent-session.d.ts.map +1 -0
  24. package/dist/core/agent-session.js +735 -0
  25. package/dist/core/agent-session.js.map +1 -0
  26. package/dist/core/bash-executor.d.ts +41 -0
  27. package/dist/core/bash-executor.d.ts.map +1 -0
  28. package/dist/core/bash-executor.js +132 -0
  29. package/dist/core/bash-executor.js.map +1 -0
  30. package/dist/{compaction.d.ts → core/compaction.d.ts} +5 -1
  31. package/dist/core/compaction.d.ts.map +1 -0
  32. package/dist/{compaction.js → core/compaction.js} +23 -1
  33. package/dist/core/compaction.js.map +1 -0
  34. package/dist/core/export-html.d.ts.map +1 -0
  35. package/dist/{export-html.js → core/export-html.js} +1 -1
  36. package/dist/{export-html.d.ts.map → core/export-html.js.map} +1 -1
  37. package/dist/core/index.d.ts +6 -0
  38. package/dist/core/index.d.ts.map +1 -0
  39. package/dist/core/index.js +6 -0
  40. package/dist/core/index.js.map +1 -0
  41. package/dist/core/messages.d.ts.map +1 -0
  42. package/dist/core/messages.js.map +1 -0
  43. package/dist/core/model-config.d.ts.map +1 -0
  44. package/dist/{model-config.js → core/model-config.js} +1 -1
  45. package/dist/core/model-config.js.map +1 -0
  46. package/dist/core/model-resolver.d.ts +48 -0
  47. package/dist/core/model-resolver.d.ts.map +1 -0
  48. package/dist/core/model-resolver.js +244 -0
  49. package/dist/core/model-resolver.js.map +1 -0
  50. package/dist/core/oauth/anthropic.d.ts.map +1 -0
  51. package/dist/core/oauth/anthropic.js.map +1 -0
  52. package/dist/core/oauth/index.d.ts.map +1 -0
  53. package/dist/{oauth/index.d.ts.map → core/oauth/index.js.map} +1 -1
  54. package/dist/core/oauth/storage.d.ts.map +1 -0
  55. package/dist/{oauth → core/oauth}/storage.js +1 -1
  56. package/dist/core/oauth/storage.js.map +1 -0
  57. package/dist/core/session-manager.d.ts.map +1 -0
  58. package/dist/{session-manager.js → core/session-manager.js} +1 -1
  59. package/dist/core/session-manager.js.map +1 -0
  60. package/dist/core/settings-manager.d.ts.map +1 -0
  61. package/dist/{settings-manager.js → core/settings-manager.js} +1 -1
  62. package/dist/core/settings-manager.js.map +1 -0
  63. package/dist/core/slash-commands.d.ts.map +1 -0
  64. package/dist/{slash-commands.js → core/slash-commands.js} +1 -1
  65. package/dist/core/slash-commands.js.map +1 -0
  66. package/dist/core/system-prompt.d.ts +17 -0
  67. package/dist/core/system-prompt.d.ts.map +1 -0
  68. package/dist/core/system-prompt.js +203 -0
  69. package/dist/core/system-prompt.js.map +1 -0
  70. package/dist/core/tools/bash.d.ts.map +1 -0
  71. package/dist/{tools → core/tools}/bash.js +1 -1
  72. package/dist/core/tools/bash.js.map +1 -0
  73. package/dist/core/tools/edit.d.ts.map +1 -0
  74. package/dist/core/tools/edit.js.map +1 -0
  75. package/dist/core/tools/find.d.ts.map +1 -0
  76. package/dist/{tools → core/tools}/find.js +1 -1
  77. package/dist/core/tools/find.js.map +1 -0
  78. package/dist/core/tools/grep.d.ts.map +1 -0
  79. package/dist/{tools → core/tools}/grep.js +1 -1
  80. package/dist/core/tools/grep.js.map +1 -0
  81. package/dist/core/tools/index.d.ts.map +1 -0
  82. package/dist/core/tools/index.js.map +1 -0
  83. package/dist/core/tools/ls.d.ts.map +1 -0
  84. package/dist/core/tools/ls.js.map +1 -0
  85. package/dist/core/tools/read.d.ts.map +1 -0
  86. package/dist/core/tools/read.js.map +1 -0
  87. package/dist/core/tools/truncate.d.ts.map +1 -0
  88. package/dist/core/tools/truncate.js.map +1 -0
  89. package/dist/core/tools/write.d.ts.map +1 -0
  90. package/dist/core/tools/write.js.map +1 -0
  91. package/dist/index.d.ts +2 -2
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -2
  94. package/dist/index.js.map +1 -1
  95. package/dist/main.d.ts +3 -0
  96. package/dist/main.d.ts.map +1 -1
  97. package/dist/main.js +176 -1082
  98. package/dist/main.js.map +1 -1
  99. package/dist/modes/index.d.ts +9 -0
  100. package/dist/modes/index.d.ts.map +1 -0
  101. package/dist/modes/index.js +8 -0
  102. package/dist/modes/index.js.map +1 -0
  103. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
  104. package/dist/modes/interactive/components/assistant-message.js.map +1 -0
  105. package/dist/{tui → modes/interactive/components}/bash-execution.d.ts +1 -1
  106. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
  107. package/dist/{tui → modes/interactive/components}/bash-execution.js +1 -1
  108. package/dist/modes/interactive/components/bash-execution.js.map +1 -0
  109. package/dist/modes/interactive/components/compaction.d.ts.map +1 -0
  110. package/dist/modes/interactive/components/compaction.js.map +1 -0
  111. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -0
  112. package/dist/modes/interactive/components/custom-editor.js.map +1 -0
  113. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -0
  114. package/dist/modes/interactive/components/dynamic-border.js.map +1 -0
  115. package/dist/modes/interactive/components/footer.d.ts.map +1 -0
  116. package/dist/{tui → modes/interactive/components}/footer.js +1 -1
  117. package/dist/modes/interactive/components/footer.js.map +1 -0
  118. package/dist/{tui → modes/interactive/components}/model-selector.d.ts +1 -1
  119. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -0
  120. package/dist/{tui → modes/interactive/components}/model-selector.js +3 -3
  121. package/dist/modes/interactive/components/model-selector.js.map +1 -0
  122. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -0
  123. package/dist/{tui → modes/interactive/components}/oauth-selector.js +2 -2
  124. package/dist/modes/interactive/components/oauth-selector.js.map +1 -0
  125. package/dist/modes/interactive/components/queue-mode-selector.d.ts.map +1 -0
  126. package/dist/modes/interactive/components/queue-mode-selector.js.map +1 -0
  127. package/dist/{tui → modes/interactive/components}/session-selector.d.ts +1 -1
  128. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -0
  129. package/dist/{tui → modes/interactive/components}/session-selector.js +1 -1
  130. package/dist/modes/interactive/components/session-selector.js.map +1 -0
  131. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -0
  132. package/dist/{tui/theme-selector.d.ts.map → modes/interactive/components/theme-selector.js.map} +1 -1
  133. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -0
  134. package/dist/modes/interactive/components/thinking-selector.js.map +1 -0
  135. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
  136. package/dist/modes/interactive/components/tool-execution.js.map +1 -0
  137. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -0
  138. package/dist/modes/interactive/components/user-message-selector.js.map +1 -0
  139. package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
  140. package/dist/modes/interactive/components/user-message.js.map +1 -0
  141. package/dist/{tui/tui-renderer.d.ts → modes/interactive/interactive-mode.d.ts} +36 -38
  142. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
  143. package/dist/modes/interactive/interactive-mode.js +1217 -0
  144. package/dist/modes/interactive/interactive-mode.js.map +1 -0
  145. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  146. package/dist/{theme → modes/interactive/theme}/theme.js +1 -1
  147. package/dist/modes/interactive/theme/theme.js.map +1 -0
  148. package/dist/modes/print-mode.d.ts +21 -0
  149. package/dist/modes/print-mode.d.ts.map +1 -0
  150. package/dist/modes/print-mode.js +53 -0
  151. package/dist/modes/print-mode.js.map +1 -0
  152. package/dist/modes/rpc/rpc-client.d.ts +182 -0
  153. package/dist/modes/rpc/rpc-client.d.ts.map +1 -0
  154. package/dist/modes/rpc/rpc-client.js +362 -0
  155. package/dist/modes/rpc/rpc-client.js.map +1 -0
  156. package/dist/modes/rpc/rpc-mode.d.ts +19 -0
  157. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
  158. package/dist/modes/rpc/rpc-mode.js +204 -0
  159. package/dist/modes/rpc/rpc-mode.js.map +1 -0
  160. package/dist/modes/rpc/rpc-types.d.ts +254 -0
  161. package/dist/modes/rpc/rpc-types.d.ts.map +1 -0
  162. package/dist/modes/rpc/rpc-types.js +8 -0
  163. package/dist/modes/rpc/rpc-types.js.map +1 -0
  164. package/dist/{changelog.d.ts → utils/changelog.d.ts} +1 -1
  165. package/dist/{changelog.js.map → utils/changelog.d.ts.map} +1 -1
  166. package/dist/{changelog.js → utils/changelog.js} +1 -1
  167. package/dist/utils/changelog.js.map +1 -0
  168. package/dist/utils/clipboard.d.ts.map +1 -0
  169. package/dist/utils/clipboard.js.map +1 -0
  170. package/dist/utils/fuzzy.d.ts.map +1 -0
  171. package/dist/utils/fuzzy.js.map +1 -0
  172. package/dist/utils/shell.d.ts.map +1 -0
  173. package/dist/{shell.js → utils/shell.js} +1 -1
  174. package/dist/utils/shell.js.map +1 -0
  175. package/dist/utils/tools-manager.d.ts.map +1 -0
  176. package/dist/{tools-manager.js → utils/tools-manager.js} +1 -1
  177. package/dist/utils/tools-manager.js.map +1 -0
  178. package/package.json +6 -6
  179. package/dist/changelog.d.ts.map +0 -1
  180. package/dist/clipboard.d.ts.map +0 -1
  181. package/dist/clipboard.js.map +0 -1
  182. package/dist/compaction.d.ts.map +0 -1
  183. package/dist/compaction.js.map +0 -1
  184. package/dist/export-html.js.map +0 -1
  185. package/dist/fuzzy.d.ts.map +0 -1
  186. package/dist/fuzzy.js.map +0 -1
  187. package/dist/messages.d.ts.map +0 -1
  188. package/dist/messages.js.map +0 -1
  189. package/dist/model-config.d.ts.map +0 -1
  190. package/dist/model-config.js.map +0 -1
  191. package/dist/oauth/anthropic.d.ts.map +0 -1
  192. package/dist/oauth/anthropic.js.map +0 -1
  193. package/dist/oauth/index.js.map +0 -1
  194. package/dist/oauth/storage.d.ts.map +0 -1
  195. package/dist/oauth/storage.js.map +0 -1
  196. package/dist/session-manager.d.ts.map +0 -1
  197. package/dist/session-manager.js.map +0 -1
  198. package/dist/settings-manager.d.ts.map +0 -1
  199. package/dist/settings-manager.js.map +0 -1
  200. package/dist/shell.d.ts.map +0 -1
  201. package/dist/shell.js.map +0 -1
  202. package/dist/slash-commands.d.ts.map +0 -1
  203. package/dist/slash-commands.js.map +0 -1
  204. package/dist/theme/theme.d.ts.map +0 -1
  205. package/dist/theme/theme.js.map +0 -1
  206. package/dist/tools/bash.d.ts.map +0 -1
  207. package/dist/tools/bash.js.map +0 -1
  208. package/dist/tools/edit.d.ts.map +0 -1
  209. package/dist/tools/edit.js.map +0 -1
  210. package/dist/tools/find.d.ts.map +0 -1
  211. package/dist/tools/find.js.map +0 -1
  212. package/dist/tools/grep.d.ts.map +0 -1
  213. package/dist/tools/grep.js.map +0 -1
  214. package/dist/tools/index.d.ts.map +0 -1
  215. package/dist/tools/index.js.map +0 -1
  216. package/dist/tools/ls.d.ts.map +0 -1
  217. package/dist/tools/ls.js.map +0 -1
  218. package/dist/tools/read.d.ts.map +0 -1
  219. package/dist/tools/read.js.map +0 -1
  220. package/dist/tools/truncate.d.ts.map +0 -1
  221. package/dist/tools/truncate.js.map +0 -1
  222. package/dist/tools/write.d.ts.map +0 -1
  223. package/dist/tools/write.js.map +0 -1
  224. package/dist/tools-manager.d.ts.map +0 -1
  225. package/dist/tools-manager.js.map +0 -1
  226. package/dist/tui/assistant-message.d.ts.map +0 -1
  227. package/dist/tui/assistant-message.js.map +0 -1
  228. package/dist/tui/bash-execution.d.ts.map +0 -1
  229. package/dist/tui/bash-execution.js.map +0 -1
  230. package/dist/tui/compaction.d.ts.map +0 -1
  231. package/dist/tui/compaction.js.map +0 -1
  232. package/dist/tui/custom-editor.d.ts.map +0 -1
  233. package/dist/tui/custom-editor.js.map +0 -1
  234. package/dist/tui/dynamic-border.d.ts.map +0 -1
  235. package/dist/tui/dynamic-border.js.map +0 -1
  236. package/dist/tui/footer.d.ts.map +0 -1
  237. package/dist/tui/footer.js.map +0 -1
  238. package/dist/tui/model-selector.d.ts.map +0 -1
  239. package/dist/tui/model-selector.js.map +0 -1
  240. package/dist/tui/oauth-selector.d.ts.map +0 -1
  241. package/dist/tui/oauth-selector.js.map +0 -1
  242. package/dist/tui/queue-mode-selector.d.ts.map +0 -1
  243. package/dist/tui/queue-mode-selector.js.map +0 -1
  244. package/dist/tui/session-selector.d.ts.map +0 -1
  245. package/dist/tui/session-selector.js.map +0 -1
  246. package/dist/tui/theme-selector.js.map +0 -1
  247. package/dist/tui/thinking-selector.d.ts.map +0 -1
  248. package/dist/tui/thinking-selector.js.map +0 -1
  249. package/dist/tui/tool-execution.d.ts.map +0 -1
  250. package/dist/tui/tool-execution.js.map +0 -1
  251. package/dist/tui/tui-renderer.d.ts.map +0 -1
  252. package/dist/tui/tui-renderer.js +0 -1937
  253. package/dist/tui/tui-renderer.js.map +0 -1
  254. package/dist/tui/user-message-selector.d.ts.map +0 -1
  255. package/dist/tui/user-message-selector.js.map +0 -1
  256. package/dist/tui/user-message.d.ts.map +0 -1
  257. package/dist/tui/user-message.js.map +0 -1
  258. /package/dist/{export-html.d.ts → core/export-html.d.ts} +0 -0
  259. /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
  260. /package/dist/{messages.js → core/messages.js} +0 -0
  261. /package/dist/{model-config.d.ts → core/model-config.d.ts} +0 -0
  262. /package/dist/{oauth → core/oauth}/anthropic.d.ts +0 -0
  263. /package/dist/{oauth → core/oauth}/anthropic.js +0 -0
  264. /package/dist/{oauth → core/oauth}/index.d.ts +0 -0
  265. /package/dist/{oauth → core/oauth}/index.js +0 -0
  266. /package/dist/{oauth → core/oauth}/storage.d.ts +0 -0
  267. /package/dist/{session-manager.d.ts → core/session-manager.d.ts} +0 -0
  268. /package/dist/{settings-manager.d.ts → core/settings-manager.d.ts} +0 -0
  269. /package/dist/{slash-commands.d.ts → core/slash-commands.d.ts} +0 -0
  270. /package/dist/{tools → core/tools}/bash.d.ts +0 -0
  271. /package/dist/{tools → core/tools}/edit.d.ts +0 -0
  272. /package/dist/{tools → core/tools}/edit.js +0 -0
  273. /package/dist/{tools → core/tools}/find.d.ts +0 -0
  274. /package/dist/{tools → core/tools}/grep.d.ts +0 -0
  275. /package/dist/{tools → core/tools}/index.d.ts +0 -0
  276. /package/dist/{tools → core/tools}/index.js +0 -0
  277. /package/dist/{tools → core/tools}/ls.d.ts +0 -0
  278. /package/dist/{tools → core/tools}/ls.js +0 -0
  279. /package/dist/{tools → core/tools}/read.d.ts +0 -0
  280. /package/dist/{tools → core/tools}/read.js +0 -0
  281. /package/dist/{tools → core/tools}/truncate.d.ts +0 -0
  282. /package/dist/{tools → core/tools}/truncate.js +0 -0
  283. /package/dist/{tools → core/tools}/write.d.ts +0 -0
  284. /package/dist/{tools → core/tools}/write.js +0 -0
  285. /package/dist/{tui → modes/interactive/components}/assistant-message.d.ts +0 -0
  286. /package/dist/{tui → modes/interactive/components}/assistant-message.js +0 -0
  287. /package/dist/{tui → modes/interactive/components}/compaction.d.ts +0 -0
  288. /package/dist/{tui → modes/interactive/components}/compaction.js +0 -0
  289. /package/dist/{tui → modes/interactive/components}/custom-editor.d.ts +0 -0
  290. /package/dist/{tui → modes/interactive/components}/custom-editor.js +0 -0
  291. /package/dist/{tui → modes/interactive/components}/dynamic-border.d.ts +0 -0
  292. /package/dist/{tui → modes/interactive/components}/dynamic-border.js +0 -0
  293. /package/dist/{tui → modes/interactive/components}/footer.d.ts +0 -0
  294. /package/dist/{tui → modes/interactive/components}/oauth-selector.d.ts +0 -0
  295. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.d.ts +0 -0
  296. /package/dist/{tui → modes/interactive/components}/queue-mode-selector.js +0 -0
  297. /package/dist/{tui → modes/interactive/components}/theme-selector.d.ts +0 -0
  298. /package/dist/{tui → modes/interactive/components}/theme-selector.js +0 -0
  299. /package/dist/{tui → modes/interactive/components}/thinking-selector.d.ts +0 -0
  300. /package/dist/{tui → modes/interactive/components}/thinking-selector.js +0 -0
  301. /package/dist/{tui → modes/interactive/components}/tool-execution.d.ts +0 -0
  302. /package/dist/{tui → modes/interactive/components}/tool-execution.js +0 -0
  303. /package/dist/{tui → modes/interactive/components}/user-message-selector.d.ts +0 -0
  304. /package/dist/{tui → modes/interactive/components}/user-message-selector.js +0 -0
  305. /package/dist/{tui → modes/interactive/components}/user-message.d.ts +0 -0
  306. /package/dist/{tui → modes/interactive/components}/user-message.js +0 -0
  307. /package/dist/{theme → modes/interactive/theme}/dark.json +0 -0
  308. /package/dist/{theme → modes/interactive/theme}/light.json +0 -0
  309. /package/dist/{theme → modes/interactive/theme}/theme-schema.json +0 -0
  310. /package/dist/{theme → modes/interactive/theme}/theme.d.ts +0 -0
  311. /package/dist/{clipboard.d.ts → utils/clipboard.d.ts} +0 -0
  312. /package/dist/{clipboard.js → utils/clipboard.js} +0 -0
  313. /package/dist/{fuzzy.d.ts → utils/fuzzy.d.ts} +0 -0
  314. /package/dist/{fuzzy.js → utils/fuzzy.js} +0 -0
  315. /package/dist/{shell.d.ts → utils/shell.d.ts} +0 -0
  316. /package/dist/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
package/dist/main.js CHANGED
@@ -1,487 +1,27 @@
1
- import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
2
- import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
3
- import chalk from "chalk";
4
- import { spawn } from "child_process";
5
- import { randomBytes } from "crypto";
6
- import { createWriteStream, existsSync, readFileSync, statSync } from "fs";
7
- import { homedir, tmpdir } from "os";
8
- import { extname, join, resolve } from "path";
9
- import stripAnsi from "strip-ansi";
10
- import { getChangelogPath, getNewEntries, parseChangelog } from "./changelog.js";
11
- import { calculateContextTokens, compact, shouldCompact } from "./compaction.js";
12
- import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getAgentDir, getModelsPath, getReadmePath, VERSION, } from "./config.js";
13
- import { exportFromFile } from "./export-html.js";
14
- import { messageTransformer } from "./messages.js";
15
- import { findModel, getApiKeyForModel, getAvailableModels } from "./model-config.js";
16
- import { loadSessionFromEntries, SessionManager } from "./session-manager.js";
17
- import { SettingsManager } from "./settings-manager.js";
18
- import { getShellConfig } from "./shell.js";
19
- import { expandSlashCommand, loadSlashCommands } from "./slash-commands.js";
20
- import { initTheme } from "./theme/theme.js";
21
- import { allTools, codingTools } from "./tools/index.js";
22
- import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
23
- import { ensureTool } from "./tools-manager.js";
24
- import { SessionSelectorComponent } from "./tui/session-selector.js";
25
- import { TuiRenderer } from "./tui/tui-renderer.js";
26
- const defaultModelPerProvider = {
27
- anthropic: "claude-sonnet-4-5",
28
- openai: "gpt-5.1-codex",
29
- google: "gemini-2.5-pro",
30
- openrouter: "openai/gpt-5.1-codex",
31
- xai: "grok-4-fast-non-reasoning",
32
- groq: "openai/gpt-oss-120b",
33
- cerebras: "zai-glm-4.6",
34
- zai: "glm-4.6",
35
- };
36
- function parseArgs(args) {
37
- const result = {
38
- messages: [],
39
- fileArgs: [],
40
- };
41
- for (let i = 0; i < args.length; i++) {
42
- const arg = args[i];
43
- if (arg === "--help" || arg === "-h") {
44
- result.help = true;
45
- }
46
- else if (arg === "--mode" && i + 1 < args.length) {
47
- const mode = args[++i];
48
- if (mode === "text" || mode === "json" || mode === "rpc") {
49
- result.mode = mode;
50
- }
51
- }
52
- else if (arg === "--continue" || arg === "-c") {
53
- result.continue = true;
54
- }
55
- else if (arg === "--resume" || arg === "-r") {
56
- result.resume = true;
57
- }
58
- else if (arg === "--provider" && i + 1 < args.length) {
59
- result.provider = args[++i];
60
- }
61
- else if (arg === "--model" && i + 1 < args.length) {
62
- result.model = args[++i];
63
- }
64
- else if (arg === "--api-key" && i + 1 < args.length) {
65
- result.apiKey = args[++i];
66
- }
67
- else if (arg === "--system-prompt" && i + 1 < args.length) {
68
- result.systemPrompt = args[++i];
69
- }
70
- else if (arg === "--append-system-prompt" && i + 1 < args.length) {
71
- result.appendSystemPrompt = args[++i];
72
- }
73
- else if (arg === "--no-session") {
74
- result.noSession = true;
75
- }
76
- else if (arg === "--session" && i + 1 < args.length) {
77
- result.session = args[++i];
78
- }
79
- else if (arg === "--models" && i + 1 < args.length) {
80
- result.models = args[++i].split(",").map((s) => s.trim());
81
- }
82
- else if (arg === "--tools" && i + 1 < args.length) {
83
- const toolNames = args[++i].split(",").map((s) => s.trim());
84
- const validTools = [];
85
- for (const name of toolNames) {
86
- if (name in allTools) {
87
- validTools.push(name);
88
- }
89
- else {
90
- console.error(chalk.yellow(`Warning: Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`));
91
- }
92
- }
93
- result.tools = validTools;
94
- }
95
- else if (arg === "--thinking" && i + 1 < args.length) {
96
- const level = args[++i];
97
- if (level === "off" ||
98
- level === "minimal" ||
99
- level === "low" ||
100
- level === "medium" ||
101
- level === "high" ||
102
- level === "xhigh") {
103
- result.thinking = level;
104
- }
105
- else {
106
- console.error(chalk.yellow(`Warning: Invalid thinking level "${level}". Valid values: off, minimal, low, medium, high, xhigh`));
107
- }
108
- }
109
- else if (arg === "--print" || arg === "-p") {
110
- result.print = true;
111
- }
112
- else if (arg === "--export" && i + 1 < args.length) {
113
- result.export = args[++i];
114
- }
115
- else if (arg.startsWith("@")) {
116
- result.fileArgs.push(arg.slice(1)); // Remove @ prefix
117
- }
118
- else if (!arg.startsWith("-")) {
119
- result.messages.push(arg);
120
- }
121
- }
122
- return result;
123
- }
124
- /**
125
- * Map of file extensions to MIME types for common image formats
126
- */
127
- const IMAGE_MIME_TYPES = {
128
- ".jpg": "image/jpeg",
129
- ".jpeg": "image/jpeg",
130
- ".png": "image/png",
131
- ".gif": "image/gif",
132
- ".webp": "image/webp",
133
- };
134
- /**
135
- * Check if a file is an image based on its extension
136
- */
137
- function isImageFile(filePath) {
138
- const ext = extname(filePath).toLowerCase();
139
- return IMAGE_MIME_TYPES[ext] || null;
140
- }
141
1
  /**
142
- * Expand ~ to home directory
2
+ * Main entry point for the coding agent
143
3
  */
144
- function expandPath(filePath) {
145
- if (filePath === "~") {
146
- return homedir();
147
- }
148
- if (filePath.startsWith("~/")) {
149
- return homedir() + filePath.slice(1);
150
- }
151
- return filePath;
152
- }
153
- /**
154
- * Process @file arguments into text content and image attachments
155
- */
156
- function processFileArguments(fileArgs) {
157
- let textContent = "";
158
- const imageAttachments = [];
159
- for (const fileArg of fileArgs) {
160
- // Expand and resolve path
161
- const expandedPath = expandPath(fileArg);
162
- const absolutePath = resolve(expandedPath);
163
- // Check if file exists
164
- if (!existsSync(absolutePath)) {
165
- console.error(chalk.red(`Error: File not found: ${absolutePath}`));
166
- process.exit(1);
167
- }
168
- // Check if file is empty
169
- const stats = statSync(absolutePath);
170
- if (stats.size === 0) {
171
- // Skip empty files
172
- continue;
173
- }
174
- const mimeType = isImageFile(absolutePath);
175
- if (mimeType) {
176
- // Handle image file
177
- const content = readFileSync(absolutePath);
178
- const base64Content = content.toString("base64");
179
- const attachment = {
180
- id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
181
- type: "image",
182
- fileName: absolutePath.split("/").pop() || absolutePath,
183
- mimeType,
184
- size: stats.size,
185
- content: base64Content,
186
- };
187
- imageAttachments.push(attachment);
188
- // Add text reference to image
189
- textContent += `<file name="${absolutePath}"></file>\n`;
190
- }
191
- else {
192
- // Handle text file
193
- try {
194
- const content = readFileSync(absolutePath, "utf-8");
195
- textContent += `<file name="${absolutePath}">\n${content}\n</file>\n`;
196
- }
197
- catch (error) {
198
- console.error(chalk.red(`Error: Could not read file ${absolutePath}: ${error.message}`));
199
- process.exit(1);
200
- }
201
- }
202
- }
203
- return { textContent, imageAttachments };
204
- }
205
- function printHelp() {
206
- console.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools
207
-
208
- ${chalk.bold("Usage:")}
209
- ${APP_NAME} [options] [@files...] [messages...]
210
-
211
- ${chalk.bold("Options:")}
212
- --provider <name> Provider name (default: google)
213
- --model <id> Model ID (default: gemini-2.5-flash)
214
- --api-key <key> API key (defaults to env vars)
215
- --system-prompt <text> System prompt (default: coding assistant prompt)
216
- --append-system-prompt <text> Append text or file contents to the system prompt
217
- --mode <mode> Output mode: text (default), json, or rpc
218
- --print, -p Non-interactive mode: process prompt and exit
219
- --continue, -c Continue previous session
220
- --resume, -r Select a session to resume
221
- --session <path> Use specific session file
222
- --no-session Don't save session (ephemeral)
223
- --models <patterns> Comma-separated model patterns for quick cycling with Ctrl+P
224
- --tools <tools> Comma-separated list of tools to enable (default: read,bash,edit,write)
225
- Available: read, bash, edit, write, grep, find, ls
226
- --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
227
- --export <file> Export session file to HTML and exit
228
- --help, -h Show this help
229
-
230
- ${chalk.bold("Examples:")}
231
- # Interactive mode
232
- ${APP_NAME}
233
-
234
- # Interactive mode with initial prompt
235
- ${APP_NAME} "List all .ts files in src/"
236
-
237
- # Include files in initial message
238
- ${APP_NAME} @prompt.md @image.png "What color is the sky?"
239
-
240
- # Non-interactive mode (process and exit)
241
- ${APP_NAME} -p "List all .ts files in src/"
242
-
243
- # Multiple messages (interactive)
244
- ${APP_NAME} "Read package.json" "What dependencies do we have?"
245
-
246
- # Continue previous session
247
- ${APP_NAME} --continue "What did we discuss?"
248
-
249
- # Use different model
250
- ${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code"
251
-
252
- # Limit model cycling to specific models
253
- ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o
254
-
255
- # Cycle models with fixed thinking levels
256
- ${APP_NAME} --models sonnet:high,haiku:low
257
-
258
- # Start with a specific thinking level
259
- ${APP_NAME} --thinking high "Solve this complex problem"
260
-
261
- # Read-only mode (no file modifications possible)
262
- ${APP_NAME} --tools read,grep,find,ls -p "Review the code in src/"
263
-
264
- # Export a session file to HTML
265
- ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl
266
- ${APP_NAME} --export session.jsonl output.html
267
-
268
- ${chalk.bold("Environment Variables:")}
269
- ANTHROPIC_API_KEY - Anthropic Claude API key
270
- ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
271
- OPENAI_API_KEY - OpenAI GPT API key
272
- GEMINI_API_KEY - Google Gemini API key
273
- GROQ_API_KEY - Groq API key
274
- CEREBRAS_API_KEY - Cerebras API key
275
- XAI_API_KEY - xAI Grok API key
276
- OPENROUTER_API_KEY - OpenRouter API key
277
- ZAI_API_KEY - ZAI API key
278
- ${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
279
-
280
- ${chalk.bold("Available Tools (default: read, bash, edit, write):")}
281
- read - Read file contents
282
- bash - Execute bash commands
283
- edit - Edit files with find/replace
284
- write - Write files (creates/overwrites)
285
- grep - Search file contents (read-only, off by default)
286
- find - Find files by glob pattern (read-only, off by default)
287
- ls - List directory contents (read-only, off by default)
288
- `);
289
- }
290
- // Tool descriptions for system prompt
291
- const toolDescriptions = {
292
- read: "Read file contents",
293
- bash: "Execute bash commands (ls, grep, find, etc.)",
294
- edit: "Make surgical edits to files (find exact text and replace)",
295
- write: "Create or overwrite files",
296
- grep: "Search file contents for patterns (respects .gitignore)",
297
- find: "Find files by glob pattern (respects .gitignore)",
298
- ls: "List directory contents",
299
- };
300
- function resolvePromptInput(input, description) {
301
- if (!input) {
302
- return undefined;
303
- }
304
- if (existsSync(input)) {
305
- try {
306
- return readFileSync(input, "utf-8");
307
- }
308
- catch (error) {
309
- console.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));
310
- return input;
311
- }
312
- }
313
- return input;
314
- }
315
- function buildSystemPrompt(customPrompt, selectedTools, appendSystemPrompt) {
316
- const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
317
- const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
318
- const now = new Date();
319
- const dateTime = now.toLocaleString("en-US", {
320
- weekday: "long",
321
- year: "numeric",
322
- month: "long",
323
- day: "numeric",
324
- hour: "2-digit",
325
- minute: "2-digit",
326
- second: "2-digit",
327
- timeZoneName: "short",
328
- });
329
- const appendSection = resolvedAppendPrompt ? `\n\n${resolvedAppendPrompt}` : "";
330
- if (resolvedCustomPrompt) {
331
- let prompt = resolvedCustomPrompt;
332
- if (appendSection) {
333
- prompt += appendSection;
334
- }
335
- // Append project context files
336
- const contextFiles = loadProjectContextFiles();
337
- if (contextFiles.length > 0) {
338
- prompt += "\n\n# Project Context\n\n";
339
- prompt += "The following project context files have been loaded:\n\n";
340
- for (const { path: filePath, content } of contextFiles) {
341
- prompt += `## ${filePath}\n\n${content}\n\n`;
342
- }
343
- }
344
- // Add date/time and working directory last
345
- prompt += `\nCurrent date and time: ${dateTime}`;
346
- prompt += `\nCurrent working directory: ${process.cwd()}`;
347
- return prompt;
348
- }
349
- // Get absolute path to README.md
350
- const readmePath = getReadmePath();
351
- // Build tools list based on selected tools
352
- const tools = selectedTools || ["read", "bash", "edit", "write"];
353
- const toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
354
- // Build guidelines based on which tools are actually available
355
- const guidelinesList = [];
356
- const hasBash = tools.includes("bash");
357
- const hasEdit = tools.includes("edit");
358
- const hasWrite = tools.includes("write");
359
- const hasGrep = tools.includes("grep");
360
- const hasFind = tools.includes("find");
361
- const hasLs = tools.includes("ls");
362
- const hasRead = tools.includes("read");
363
- // Read-only mode notice (no bash, edit, or write)
364
- if (!hasBash && !hasEdit && !hasWrite) {
365
- guidelinesList.push("You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands");
366
- }
367
- // Bash without edit/write = read-only bash mode
368
- if (hasBash && !hasEdit && !hasWrite) {
369
- guidelinesList.push("Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files");
370
- }
371
- // File exploration guidelines
372
- if (hasBash && !hasGrep && !hasFind && !hasLs) {
373
- guidelinesList.push("Use bash for file operations like ls, grep, find");
374
- }
375
- else if (hasBash && (hasGrep || hasFind || hasLs)) {
376
- guidelinesList.push("Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)");
377
- }
378
- // Read before edit guideline
379
- if (hasRead && hasEdit) {
380
- guidelinesList.push("Use read to examine files before editing");
381
- }
382
- // Edit guideline
383
- if (hasEdit) {
384
- guidelinesList.push("Use edit for precise changes (old text must match exactly)");
385
- }
386
- // Write guideline
387
- if (hasWrite) {
388
- guidelinesList.push("Use write only for new files or complete rewrites");
389
- }
390
- // Output guideline (only when actually writing/executing)
391
- if (hasEdit || hasWrite) {
392
- guidelinesList.push("When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did");
393
- }
394
- // Always include these
395
- guidelinesList.push("Be concise in your responses");
396
- guidelinesList.push("Show file paths clearly when working with files");
397
- const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
398
- let prompt = `You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
399
-
400
- Available tools:
401
- ${toolsList}
402
-
403
- Guidelines:
404
- ${guidelines}
405
-
406
- Documentation:
407
- - Your own documentation (including custom model setup and theme creation) is at: ${readmePath}
408
- - Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.`;
409
- if (appendSection) {
410
- prompt += appendSection;
411
- }
412
- // Append project context files
413
- const contextFiles = loadProjectContextFiles();
414
- if (contextFiles.length > 0) {
415
- prompt += "\n\n# Project Context\n\n";
416
- prompt += "The following project context files have been loaded:\n\n";
417
- for (const { path: filePath, content } of contextFiles) {
418
- prompt += `## ${filePath}\n\n${content}\n\n`;
419
- }
420
- }
421
- // Add date/time and working directory last
422
- prompt += `\nCurrent date and time: ${dateTime}`;
423
- prompt += `\nCurrent working directory: ${process.cwd()}`;
424
- return prompt;
425
- }
426
- /**
427
- * Look for AGENTS.md or CLAUDE.md in a directory (prefers AGENTS.md)
428
- */
429
- function loadContextFileFromDir(dir) {
430
- const candidates = ["AGENTS.md", "CLAUDE.md"];
431
- for (const filename of candidates) {
432
- const filePath = join(dir, filename);
433
- if (existsSync(filePath)) {
434
- try {
435
- return {
436
- path: filePath,
437
- content: readFileSync(filePath, "utf-8"),
438
- };
439
- }
440
- catch (error) {
441
- console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));
442
- }
443
- }
444
- }
445
- return null;
446
- }
447
- /**
448
- * Load all project context files in order:
449
- * 1. Global: ~/{CONFIG_DIR_NAME}/agent/AGENTS.md or CLAUDE.md
450
- * 2. Parent directories (top-most first) down to cwd
451
- * Each returns {path, content} for separate messages
452
- */
453
- function loadProjectContextFiles() {
454
- const contextFiles = [];
455
- // 1. Load global context from ~/{CONFIG_DIR_NAME}/agent/
456
- const globalContextDir = getAgentDir();
457
- const globalContext = loadContextFileFromDir(globalContextDir);
458
- if (globalContext) {
459
- contextFiles.push(globalContext);
460
- }
461
- // 2. Walk up from cwd to root, collecting all context files
462
- const cwd = process.cwd();
463
- const ancestorContextFiles = [];
464
- let currentDir = cwd;
465
- const root = resolve("/");
466
- while (true) {
467
- const contextFile = loadContextFileFromDir(currentDir);
468
- if (contextFile) {
469
- // Add to beginning so we get top-most parent first
470
- ancestorContextFiles.unshift(contextFile);
471
- }
472
- // Stop if we've reached root
473
- if (currentDir === root)
474
- break;
475
- // Move up one directory
476
- const parentDir = resolve(currentDir, "..");
477
- if (parentDir === currentDir)
478
- break; // Safety check
479
- currentDir = parentDir;
480
- }
481
- // Add ancestor files in order (top-most → cwd)
482
- contextFiles.push(...ancestorContextFiles);
483
- return contextFiles;
484
- }
4
+ import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
5
+ import chalk from "chalk";
6
+ import { parseArgs, printHelp } from "./cli/args.js";
7
+ import { processFileArguments } from "./cli/file-processor.js";
8
+ import { selectSession } from "./cli/session-picker.js";
9
+ import { getModelsPath, VERSION } from "./config.js";
10
+ import { AgentSession } from "./core/agent-session.js";
11
+ import { exportFromFile } from "./core/export-html.js";
12
+ import { messageTransformer } from "./core/messages.js";
13
+ import { findModel, getApiKeyForModel, getAvailableModels } from "./core/model-config.js";
14
+ import { resolveModelScope, restoreModelFromSession } from "./core/model-resolver.js";
15
+ import { SessionManager } from "./core/session-manager.js";
16
+ import { SettingsManager } from "./core/settings-manager.js";
17
+ import { loadSlashCommands } from "./core/slash-commands.js";
18
+ import { buildSystemPrompt, loadProjectContextFiles } from "./core/system-prompt.js";
19
+ import { allTools, codingTools } from "./core/tools/index.js";
20
+ import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
21
+ import { initTheme } from "./modes/interactive/theme/theme.js";
22
+ import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
23
+ import { ensureTool } from "./utils/tools-manager.js";
24
+ /** Check npm registry for new version (non-blocking) */
485
25
  async function checkForNewVersion(currentVersion) {
486
26
  try {
487
27
  const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest");
@@ -494,425 +34,80 @@ async function checkForNewVersion(currentVersion) {
494
34
  }
495
35
  return null;
496
36
  }
497
- catch (error) {
37
+ catch {
498
38
  // Silently fail - don't disrupt the user experience
499
39
  return null;
500
40
  }
501
41
  }
502
- /**
503
- * Resolve model patterns to actual Model objects with optional thinking levels
504
- * Format: "pattern:level" where :level is optional
505
- * For each pattern, finds all matching models and picks the best version:
506
- * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)
507
- * 2. If no alias, pick the latest dated version
508
- */
509
- async function resolveModelScope(patterns) {
510
- const { models: availableModels, error } = await getAvailableModels();
511
- if (error) {
512
- console.warn(chalk.yellow(`Warning: Error loading models: ${error}`));
513
- return [];
514
- }
515
- const scopedModels = [];
516
- for (const pattern of patterns) {
517
- // Parse pattern:level format
518
- const parts = pattern.split(":");
519
- const modelPattern = parts[0];
520
- let thinkingLevel = "off";
521
- if (parts.length > 1) {
522
- const level = parts[1];
523
- if (level === "off" ||
524
- level === "minimal" ||
525
- level === "low" ||
526
- level === "medium" ||
527
- level === "high" ||
528
- level === "xhigh") {
529
- thinkingLevel = level;
530
- }
531
- else {
532
- console.warn(chalk.yellow(`Warning: Invalid thinking level "${level}" in pattern "${pattern}". Using "off" instead.`));
533
- }
534
- }
535
- // Check for provider/modelId format (provider is everything before the first /)
536
- const slashIndex = modelPattern.indexOf("/");
537
- if (slashIndex !== -1) {
538
- const provider = modelPattern.substring(0, slashIndex);
539
- const modelId = modelPattern.substring(slashIndex + 1);
540
- const providerMatch = availableModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase());
541
- if (providerMatch) {
542
- if (!scopedModels.find((sm) => sm.model.id === providerMatch.id && sm.model.provider === providerMatch.provider)) {
543
- scopedModels.push({ model: providerMatch, thinkingLevel });
544
- }
545
- continue;
546
- }
547
- // No exact provider/model match - fall through to other matching
548
- }
549
- // Check for exact ID match (case-insensitive)
550
- const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
551
- if (exactMatch) {
552
- // Exact match found - use it directly
553
- if (!scopedModels.find((sm) => sm.model.id === exactMatch.id && sm.model.provider === exactMatch.provider)) {
554
- scopedModels.push({ model: exactMatch, thinkingLevel });
555
- }
556
- continue;
557
- }
558
- // No exact match - fall back to partial matching
559
- const matches = availableModels.filter((m) => m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
560
- m.name?.toLowerCase().includes(modelPattern.toLowerCase()));
561
- if (matches.length === 0) {
562
- console.warn(chalk.yellow(`Warning: No models match pattern "${modelPattern}"`));
563
- continue;
564
- }
565
- // Helper to check if a model ID looks like an alias (no date suffix)
566
- // Dates are typically in format: -20241022 or -20250929
567
- const isAlias = (id) => {
568
- // Check if ID ends with -latest
569
- if (id.endsWith("-latest"))
570
- return true;
571
- // Check if ID ends with a date pattern (-YYYYMMDD)
572
- const datePattern = /-\d{8}$/;
573
- return !datePattern.test(id);
574
- };
575
- // Separate into aliases and dated versions
576
- const aliases = matches.filter((m) => isAlias(m.id));
577
- const datedVersions = matches.filter((m) => !isAlias(m.id));
578
- let bestMatch;
579
- if (aliases.length > 0) {
580
- // Prefer alias - if multiple aliases, pick the one that sorts highest
581
- aliases.sort((a, b) => b.id.localeCompare(a.id));
582
- bestMatch = aliases[0];
583
- }
584
- else {
585
- // No alias found, pick latest dated version
586
- datedVersions.sort((a, b) => b.id.localeCompare(a.id));
587
- bestMatch = datedVersions[0];
588
- }
589
- // Avoid duplicates
590
- if (!scopedModels.find((sm) => sm.model.id === bestMatch.id && sm.model.provider === bestMatch.provider)) {
591
- scopedModels.push({ model: bestMatch, thinkingLevel });
592
- }
593
- }
594
- return scopedModels;
595
- }
596
- async function selectSession(sessionManager) {
597
- return new Promise((resolve) => {
598
- const ui = new TUI(new ProcessTerminal());
599
- let resolved = false;
600
- const selector = new SessionSelectorComponent(sessionManager, (path) => {
601
- if (!resolved) {
602
- resolved = true;
603
- ui.stop();
604
- resolve(path);
605
- }
606
- }, () => {
607
- if (!resolved) {
608
- resolved = true;
609
- ui.stop();
610
- resolve(null);
611
- }
612
- });
613
- ui.addChild(selector);
614
- ui.setFocus(selector.getSessionList());
615
- ui.start();
616
- });
617
- }
618
- async function runInteractiveMode(agent, sessionManager, settingsManager, version, changelogMarkdown = null, collapseChangelog = false, modelFallbackMessage = null, versionCheckPromise, scopedModels = [], initialMessages = [], initialMessage, initialAttachments, fdPath = null) {
619
- const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown, collapseChangelog, scopedModels, fdPath);
42
+ /** Run interactive mode with TUI */
43
+ async function runInteractiveMode(session, version, changelogMarkdown, modelFallbackMessage, versionCheckPromise, initialMessages, initialMessage, initialAttachments, fdPath = null) {
44
+ const mode = new InteractiveMode(session, version, changelogMarkdown, fdPath);
620
45
  // Initialize TUI (subscribes to agent events internally)
621
- await renderer.init();
46
+ await mode.init();
622
47
  // Handle version check result when it completes (don't block)
623
48
  versionCheckPromise.then((newVersion) => {
624
49
  if (newVersion) {
625
- renderer.showNewVersionNotification(newVersion);
50
+ mode.showNewVersionNotification(newVersion);
626
51
  }
627
52
  });
628
53
  // Render any existing messages (from --continue mode)
629
- renderer.renderInitialMessages(agent.state);
54
+ mode.renderInitialMessages(session.state);
630
55
  // Show model fallback warning at the end of the chat if applicable
631
56
  if (modelFallbackMessage) {
632
- renderer.showWarning(modelFallbackMessage);
57
+ mode.showWarning(modelFallbackMessage);
633
58
  }
634
- // Load file-based slash commands for expansion
635
- const fileCommands = loadSlashCommands();
636
59
  // Process initial message with attachments if provided (from @file args)
637
60
  if (initialMessage) {
638
61
  try {
639
- await agent.prompt(expandSlashCommand(initialMessage, fileCommands), initialAttachments);
62
+ await session.prompt(initialMessage, { attachments: initialAttachments });
640
63
  }
641
64
  catch (error) {
642
65
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
643
- renderer.showError(errorMessage);
66
+ mode.showError(errorMessage);
644
67
  }
645
68
  }
646
69
  // Process remaining initial messages if provided (from CLI args)
647
70
  for (const message of initialMessages) {
648
71
  try {
649
- await agent.prompt(expandSlashCommand(message, fileCommands));
72
+ await session.prompt(message);
650
73
  }
651
74
  catch (error) {
652
75
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
653
- renderer.showError(errorMessage);
76
+ mode.showError(errorMessage);
654
77
  }
655
78
  }
656
79
  // Interactive loop
657
80
  while (true) {
658
- const userInput = await renderer.getUserInput();
659
- // Process the message - agent.prompt will add user message and trigger state updates
81
+ const userInput = await mode.getUserInput();
82
+ // Process the message
660
83
  try {
661
- await agent.prompt(userInput);
84
+ await session.prompt(userInput);
662
85
  }
663
86
  catch (error) {
664
- // Display error in the TUI by adding an error message to the chat
665
87
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
666
- renderer.showError(errorMessage);
88
+ mode.showError(errorMessage);
667
89
  }
668
90
  }
669
91
  }
670
- async function runSingleShotMode(agent, _sessionManager, messages, mode, initialMessage, initialAttachments) {
671
- // Load file-based slash commands for expansion
672
- const fileCommands = loadSlashCommands();
673
- if (mode === "json") {
674
- // Subscribe to all events and output as JSON
675
- agent.subscribe((event) => {
676
- // Output event as JSON (same format as session manager)
677
- console.log(JSON.stringify(event));
678
- });
679
- }
680
- // Send initial message with attachments if provided
681
- if (initialMessage) {
682
- await agent.prompt(expandSlashCommand(initialMessage, fileCommands), initialAttachments);
92
+ /** Prepare initial message from @file arguments */
93
+ function prepareInitialMessage(parsed) {
94
+ if (parsed.fileArgs.length === 0) {
95
+ return {};
683
96
  }
684
- // Send remaining messages
685
- for (const message of messages) {
686
- await agent.prompt(expandSlashCommand(message, fileCommands));
97
+ const { textContent, imageAttachments } = processFileArguments(parsed.fileArgs);
98
+ // Combine file content with first plain text message (if any)
99
+ let initialMessage;
100
+ if (parsed.messages.length > 0) {
101
+ initialMessage = textContent + parsed.messages[0];
102
+ parsed.messages.shift(); // Remove first message as it's been combined
687
103
  }
688
- // In text mode, only output the final assistant message
689
- if (mode === "text") {
690
- const lastMessage = agent.state.messages[agent.state.messages.length - 1];
691
- if (lastMessage.role === "assistant") {
692
- const assistantMsg = lastMessage;
693
- // Check for error/aborted and output error message
694
- if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
695
- console.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
696
- process.exit(1);
697
- }
698
- for (const content of assistantMsg.content) {
699
- if (content.type === "text") {
700
- console.log(content.text);
701
- }
702
- }
703
- }
104
+ else {
105
+ initialMessage = textContent;
704
106
  }
705
- }
706
- /**
707
- * Execute a bash command for RPC mode.
708
- * Similar to tui-renderer's executeBashCommand but without streaming callbacks.
709
- */
710
- async function executeRpcBashCommand(command) {
711
- return new Promise((resolve, reject) => {
712
- const { shell, args } = getShellConfig();
713
- const child = spawn(shell, [...args, command], {
714
- detached: true,
715
- stdio: ["ignore", "pipe", "pipe"],
716
- });
717
- const chunks = [];
718
- let chunksBytes = 0;
719
- const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
720
- let tempFilePath;
721
- let tempFileStream;
722
- let totalBytes = 0;
723
- const handleData = (data) => {
724
- totalBytes += data.length;
725
- // Start writing to temp file if exceeds threshold
726
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
727
- const id = randomBytes(8).toString("hex");
728
- tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
729
- tempFileStream = createWriteStream(tempFilePath);
730
- for (const chunk of chunks) {
731
- tempFileStream.write(chunk);
732
- }
733
- }
734
- if (tempFileStream) {
735
- tempFileStream.write(data);
736
- }
737
- // Keep rolling buffer
738
- chunks.push(data);
739
- chunksBytes += data.length;
740
- while (chunksBytes > maxChunksBytes && chunks.length > 1) {
741
- const removed = chunks.shift();
742
- chunksBytes -= removed.length;
743
- }
744
- };
745
- child.stdout?.on("data", handleData);
746
- child.stderr?.on("data", handleData);
747
- child.on("close", (code) => {
748
- if (tempFileStream) {
749
- tempFileStream.end();
750
- }
751
- // Combine buffered chunks
752
- const fullBuffer = Buffer.concat(chunks);
753
- const fullOutput = stripAnsi(fullBuffer.toString("utf-8")).replace(/\r/g, "");
754
- const truncationResult = truncateTail(fullOutput);
755
- resolve({
756
- output: fullOutput,
757
- exitCode: code,
758
- truncationResult: truncationResult.truncated ? truncationResult : undefined,
759
- fullOutputPath: tempFilePath,
760
- });
761
- });
762
- child.on("error", (err) => {
763
- if (tempFileStream) {
764
- tempFileStream.end();
765
- }
766
- reject(err);
767
- });
768
- });
769
- }
770
- async function runRpcMode(agent, sessionManager, settingsManager) {
771
- // Track if auto-compaction is in progress
772
- let autoCompactionInProgress = false;
773
- // Auto-compaction helper
774
- const checkAutoCompaction = async () => {
775
- if (autoCompactionInProgress)
776
- return;
777
- const settings = settingsManager.getCompactionSettings();
778
- if (!settings.enabled)
779
- return;
780
- // Get last non-aborted assistant message
781
- const messages = agent.state.messages;
782
- let lastAssistant = null;
783
- for (let i = messages.length - 1; i >= 0; i--) {
784
- const msg = messages[i];
785
- if (msg.role === "assistant") {
786
- const assistantMsg = msg;
787
- if (assistantMsg.stopReason !== "aborted") {
788
- lastAssistant = assistantMsg;
789
- break;
790
- }
791
- }
792
- }
793
- if (!lastAssistant)
794
- return;
795
- const contextTokens = calculateContextTokens(lastAssistant.usage);
796
- const contextWindow = agent.state.model.contextWindow;
797
- if (!shouldCompact(contextTokens, contextWindow, settings))
798
- return;
799
- // Trigger auto-compaction
800
- autoCompactionInProgress = true;
801
- try {
802
- const apiKey = await getApiKeyForModel(agent.state.model);
803
- if (!apiKey) {
804
- throw new Error(`No API key for ${agent.state.model.provider}`);
805
- }
806
- const entries = sessionManager.loadEntries();
807
- const compactionEntry = await compact(entries, agent.state.model, settings, apiKey);
808
- sessionManager.saveCompaction(compactionEntry);
809
- const loaded = loadSessionFromEntries(sessionManager.loadEntries());
810
- agent.replaceMessages(loaded.messages);
811
- // Emit auto-compaction event
812
- console.log(JSON.stringify({ ...compactionEntry, auto: true }));
813
- }
814
- catch (error) {
815
- const message = error instanceof Error ? error.message : String(error);
816
- console.log(JSON.stringify({ type: "error", error: `Auto-compaction failed: ${message}` }));
817
- }
818
- finally {
819
- autoCompactionInProgress = false;
820
- }
107
+ return {
108
+ initialMessage,
109
+ initialAttachments: imageAttachments.length > 0 ? imageAttachments : undefined,
821
110
  };
822
- // Subscribe to all events and output as JSON (same pattern as tui-renderer)
823
- agent.subscribe(async (event) => {
824
- console.log(JSON.stringify(event));
825
- // Save messages to session
826
- if (event.type === "message_end") {
827
- sessionManager.saveMessage(event.message);
828
- // Yield to microtask queue to allow agent state to update
829
- // (tui-renderer does this implicitly via await handleEvent)
830
- await Promise.resolve();
831
- // Check if we should initialize session now (after first user+assistant exchange)
832
- if (sessionManager.shouldInitializeSession(agent.state.messages)) {
833
- sessionManager.startSession(agent.state);
834
- }
835
- // Check for auto-compaction after assistant messages
836
- if (event.message.role === "assistant") {
837
- await checkAutoCompaction();
838
- }
839
- }
840
- });
841
- // Listen for JSON input on stdin
842
- const readline = await import("readline");
843
- const rl = readline.createInterface({
844
- input: process.stdin,
845
- output: process.stdout,
846
- terminal: false,
847
- });
848
- rl.on("line", async (line) => {
849
- try {
850
- const input = JSON.parse(line);
851
- // Handle different RPC commands
852
- if (input.type === "prompt" && input.message) {
853
- await agent.prompt(input.message, input.attachments);
854
- }
855
- else if (input.type === "abort") {
856
- agent.abort();
857
- }
858
- else if (input.type === "compact") {
859
- // Handle compaction request
860
- try {
861
- const apiKey = await getApiKeyForModel(agent.state.model);
862
- if (!apiKey) {
863
- throw new Error(`No API key for ${agent.state.model.provider}`);
864
- }
865
- const entries = sessionManager.loadEntries();
866
- const settings = settingsManager.getCompactionSettings();
867
- const compactionEntry = await compact(entries, agent.state.model, settings, apiKey, undefined, input.customInstructions);
868
- // Save and reload
869
- sessionManager.saveCompaction(compactionEntry);
870
- const loaded = loadSessionFromEntries(sessionManager.loadEntries());
871
- agent.replaceMessages(loaded.messages);
872
- // Emit compaction event (compactionEntry already has type: "compaction")
873
- console.log(JSON.stringify(compactionEntry));
874
- }
875
- catch (error) {
876
- console.log(JSON.stringify({ type: "error", error: `Compaction failed: ${error.message}` }));
877
- }
878
- }
879
- else if (input.type === "bash" && input.command) {
880
- // Execute bash command and add to context
881
- try {
882
- const result = await executeRpcBashCommand(input.command);
883
- // Create bash execution message
884
- const bashMessage = {
885
- role: "bashExecution",
886
- command: input.command,
887
- output: result.truncationResult?.content || result.output,
888
- exitCode: result.exitCode,
889
- cancelled: false,
890
- truncated: result.truncationResult?.truncated || false,
891
- fullOutputPath: result.fullOutputPath,
892
- timestamp: Date.now(),
893
- };
894
- // Add to agent state and save to session
895
- agent.appendMessage(bashMessage);
896
- sessionManager.saveMessage(bashMessage);
897
- // Initialize session if needed (same logic as message_end handler)
898
- if (sessionManager.shouldInitializeSession(agent.state.messages)) {
899
- sessionManager.startSession(agent.state);
900
- }
901
- // Emit bash_end event with the message
902
- console.log(JSON.stringify({ type: "bash_end", message: bashMessage }));
903
- }
904
- catch (error) {
905
- console.log(JSON.stringify({ type: "error", error: `Bash command failed: ${error.message}` }));
906
- }
907
- }
908
- }
909
- catch (error) {
910
- // Output error as JSON
911
- console.log(JSON.stringify({ type: "error", error: error.message }));
912
- }
913
- });
914
- // Keep process alive
915
- return new Promise(() => { });
916
111
  }
917
112
  export async function main(args) {
918
113
  const parsed = parseArgs(args);
@@ -923,14 +118,14 @@ export async function main(args) {
923
118
  // Handle --export flag: convert session file to HTML and exit
924
119
  if (parsed.export) {
925
120
  try {
926
- // Use first message as output path if provided
927
121
  const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;
928
122
  const result = exportFromFile(parsed.export, outputPath);
929
123
  console.log(`Exported to: ${result}`);
930
124
  return;
931
125
  }
932
126
  catch (error) {
933
- console.error(chalk.red(`Error: ${error.message || "Failed to export session"}`));
127
+ const message = error instanceof Error ? error.message : "Failed to export session";
128
+ console.error(chalk.red(`Error: ${message}`));
934
129
  process.exit(1);
935
130
  }
936
131
  }
@@ -939,28 +134,14 @@ export async function main(args) {
939
134
  console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
940
135
  process.exit(1);
941
136
  }
942
- // Process @file arguments if any
943
- let initialMessage;
944
- let initialAttachments;
945
- if (parsed.fileArgs.length > 0) {
946
- const { textContent, imageAttachments } = processFileArguments(parsed.fileArgs);
947
- // Combine file content with first plain text message (if any)
948
- if (parsed.messages.length > 0) {
949
- initialMessage = textContent + parsed.messages[0];
950
- parsed.messages.shift(); // Remove first message as it's been combined
951
- }
952
- else {
953
- initialMessage = textContent;
954
- }
955
- initialAttachments = imageAttachments.length > 0 ? imageAttachments : undefined;
956
- }
137
+ // Process @file arguments
138
+ const { initialMessage, initialAttachments } = prepareInitialMessage(parsed);
957
139
  // Initialize theme (before any TUI rendering)
958
140
  const settingsManager = new SettingsManager();
959
141
  const themeName = settingsManager.getTheme();
960
142
  initTheme(themeName);
961
143
  // Setup session manager
962
144
  const sessionManager = new SessionManager(parsed.continue && !parsed.resume, parsed.session);
963
- // Disable session saving if --no-session flag is set
964
145
  if (parsed.noSession) {
965
146
  sessionManager.disable();
966
147
  }
@@ -971,95 +152,31 @@ export async function main(args) {
971
152
  console.log(chalk.dim("No session selected"));
972
153
  return;
973
154
  }
974
- // Set the selected session as the active session
975
155
  sessionManager.setSessionFile(selectedSession);
976
156
  }
977
- // Resolve model scope early if provided (needed for initial model selection)
157
+ // Resolve model scope early if provided
978
158
  let scopedModels = [];
979
159
  if (parsed.models && parsed.models.length > 0) {
980
160
  scopedModels = await resolveModelScope(parsed.models);
981
161
  }
982
- // Determine initial model using priority system:
983
- // 1. CLI args (--provider and --model)
984
- // 2. First model from --models scope
985
- // 3. Restored from session (if --continue or --resume)
986
- // 4. Saved default from settings.json
987
- // 5. First available model with valid API key
988
- // 6. null (allowed in interactive mode)
989
- let initialModel = null;
162
+ // Determine mode and output behavior
163
+ const isInteractive = !parsed.print && parsed.mode === undefined;
164
+ const mode = parsed.mode || "text";
165
+ const shouldPrintMessages = isInteractive;
166
+ // Find initial model
167
+ let initialModel = await findInitialModelForSession(parsed, scopedModels, settingsManager);
990
168
  let initialThinking = "off";
991
- if (parsed.provider && parsed.model) {
992
- // 1. CLI args take priority
993
- const { model, error } = findModel(parsed.provider, parsed.model);
994
- if (error) {
995
- console.error(chalk.red(error));
996
- process.exit(1);
997
- }
998
- if (!model) {
999
- console.error(chalk.red(`Model ${parsed.provider}/${parsed.model} not found`));
1000
- process.exit(1);
1001
- }
1002
- initialModel = model;
1003
- }
1004
- else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
1005
- // 2. Use first model from --models scope (skip if continuing/resuming session)
1006
- initialModel = scopedModels[0].model;
169
+ // Get thinking level from scoped models if applicable
170
+ if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
1007
171
  initialThinking = scopedModels[0].thinkingLevel;
1008
172
  }
1009
- else if (parsed.continue || parsed.resume) {
1010
- // 3. Restore from session (will be handled below after loading session)
1011
- // Leave initialModel as null for now
1012
- }
1013
- if (!initialModel) {
1014
- // 3. Try saved default from settings
1015
- const defaultProvider = settingsManager.getDefaultProvider();
1016
- const defaultModel = settingsManager.getDefaultModel();
1017
- if (defaultProvider && defaultModel) {
1018
- const { model, error } = findModel(defaultProvider, defaultModel);
1019
- if (error) {
1020
- console.error(chalk.red(error));
1021
- process.exit(1);
1022
- }
1023
- initialModel = model;
1024
- // Also load saved thinking level if we're using saved model
1025
- const savedThinking = settingsManager.getDefaultThinkingLevel();
1026
- if (savedThinking) {
1027
- initialThinking = savedThinking;
1028
- }
1029
- }
1030
- }
1031
- if (!initialModel) {
1032
- // 4. Try first available model with valid API key
1033
- // Prefer default model for each provider if available
1034
- const { models: availableModels, error } = await getAvailableModels();
1035
- if (error) {
1036
- console.error(chalk.red(error));
1037
- process.exit(1);
1038
- }
1039
- if (availableModels.length > 0) {
1040
- // Try to find a default model from known providers
1041
- for (const provider of Object.keys(defaultModelPerProvider)) {
1042
- const defaultModelId = defaultModelPerProvider[provider];
1043
- const match = availableModels.find((m) => m.provider === provider && m.id === defaultModelId);
1044
- if (match) {
1045
- initialModel = match;
1046
- break;
1047
- }
1048
- }
1049
- // If no default found, use first available
1050
- if (!initialModel) {
1051
- initialModel = availableModels[0];
1052
- }
173
+ else {
174
+ // Try saved thinking level
175
+ const savedThinking = settingsManager.getDefaultThinkingLevel();
176
+ if (savedThinking) {
177
+ initialThinking = savedThinking;
1053
178
  }
1054
179
  }
1055
- // Determine mode early to know if we should print messages and fail early
1056
- // Interactive mode: no --print flag and no --mode flag
1057
- // Having initial messages doesn't make it non-interactive anymore
1058
- const isInteractive = !parsed.print && parsed.mode === undefined;
1059
- const mode = parsed.mode || "text";
1060
- // Only print informational messages in interactive mode
1061
- // Non-interactive modes (-p, --mode json, --mode rpc) should be silent except for output
1062
- const shouldPrintMessages = isInteractive;
1063
180
  // Non-interactive mode: fail early if no model available
1064
181
  if (!isInteractive && !initialModel) {
1065
182
  console.error(chalk.red("No models available."));
@@ -1076,71 +193,25 @@ export async function main(args) {
1076
193
  process.exit(1);
1077
194
  }
1078
195
  }
196
+ // Build system prompt
1079
197
  const systemPrompt = buildSystemPrompt(parsed.systemPrompt, parsed.tools, parsed.appendSystemPrompt);
1080
- // Load previous messages if continuing or resuming
1081
- // This may update initialModel if restoring from session
1082
- if (parsed.continue || parsed.resume) {
1083
- // Load and restore model (overrides initialModel if found and has API key)
198
+ // Handle session restoration
199
+ let modelFallbackMessage = null;
200
+ if (parsed.continue || parsed.resume || parsed.session) {
1084
201
  const savedModel = sessionManager.loadModel();
1085
202
  if (savedModel) {
1086
- const { model: restoredModel, error } = findModel(savedModel.provider, savedModel.modelId);
1087
- if (error) {
1088
- console.error(chalk.red(error));
1089
- process.exit(1);
203
+ const result = await restoreModelFromSession(savedModel.provider, savedModel.modelId, initialModel, shouldPrintMessages);
204
+ if (result.model) {
205
+ initialModel = result.model;
1090
206
  }
1091
- // Check if restored model exists and has a valid API key
1092
- const hasApiKey = restoredModel ? !!(await getApiKeyForModel(restoredModel)) : false;
1093
- if (restoredModel && hasApiKey) {
1094
- initialModel = restoredModel;
1095
- if (shouldPrintMessages) {
1096
- console.log(chalk.dim(`Restored model: ${savedModel.provider}/${savedModel.modelId}`));
1097
- }
1098
- }
1099
- else {
1100
- // Model not found or no API key - fall back to default selection
1101
- const reason = !restoredModel ? "model no longer exists" : "no API key available";
1102
- if (shouldPrintMessages) {
1103
- console.error(chalk.yellow(`Warning: Could not restore model ${savedModel.provider}/${savedModel.modelId} (${reason}).`));
1104
- }
1105
- // Ensure we have a valid model - use the same fallback logic
1106
- if (!initialModel) {
1107
- const { models: availableModels, error: availableError } = await getAvailableModels();
1108
- if (availableError) {
1109
- console.error(chalk.red(availableError));
1110
- process.exit(1);
1111
- }
1112
- if (availableModels.length > 0) {
1113
- // Try to find a default model from known providers
1114
- for (const provider of Object.keys(defaultModelPerProvider)) {
1115
- const defaultModelId = defaultModelPerProvider[provider];
1116
- const match = availableModels.find((m) => m.provider === provider && m.id === defaultModelId);
1117
- if (match) {
1118
- initialModel = match;
1119
- break;
1120
- }
1121
- }
1122
- // If no default found, use first available
1123
- if (!initialModel) {
1124
- initialModel = availableModels[0];
1125
- }
1126
- if (initialModel && shouldPrintMessages) {
1127
- console.log(chalk.dim(`Falling back to: ${initialModel.provider}/${initialModel.id}`));
1128
- }
1129
- }
1130
- else {
1131
- // No models available at all
1132
- if (shouldPrintMessages) {
1133
- console.error(chalk.red("\nNo models available."));
1134
- console.error(chalk.yellow("Set an API key environment variable:"));
1135
- console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
1136
- console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
1137
- }
1138
- process.exit(1);
1139
- }
1140
- }
1141
- else if (shouldPrintMessages) {
1142
- console.log(chalk.dim(`Falling back to: ${initialModel.provider}/${initialModel.id}`));
1143
- }
207
+ modelFallbackMessage = result.fallbackMessage;
208
+ }
209
+ // Load and restore thinking level
210
+ const thinkingLevel = sessionManager.loadThinkingLevel();
211
+ if (thinkingLevel) {
212
+ initialThinking = thinkingLevel;
213
+ if (shouldPrintMessages) {
214
+ console.log(chalk.dim(`Restored thinking level: ${thinkingLevel}`));
1144
215
  }
1145
216
  }
1146
217
  }
@@ -1150,28 +221,25 @@ export async function main(args) {
1150
221
  }
1151
222
  // Determine which tools to use
1152
223
  const selectedTools = parsed.tools ? parsed.tools.map((name) => allTools[name]) : codingTools;
1153
- // Create agent (initialModel can be null in interactive mode)
224
+ // Create agent
1154
225
  const agent = new Agent({
1155
226
  initialState: {
1156
227
  systemPrompt,
1157
- model: initialModel, // Can be null
228
+ model: initialModel, // Can be null in interactive mode
1158
229
  thinkingLevel: initialThinking,
1159
230
  tools: selectedTools,
1160
231
  },
1161
232
  messageTransformer,
1162
233
  queueMode: settingsManager.getQueueMode(),
1163
234
  transport: new ProviderTransport({
1164
- // Dynamic API key lookup based on current model's provider
1165
235
  getApiKey: async () => {
1166
236
  const currentModel = agent.state.model;
1167
237
  if (!currentModel) {
1168
238
  throw new Error("No model selected");
1169
239
  }
1170
- // Try CLI override first
1171
240
  if (parsed.apiKey) {
1172
241
  return parsed.apiKey;
1173
242
  }
1174
- // Use model-specific key lookup
1175
243
  const key = await getApiKeyForModel(currentModel);
1176
244
  if (!key) {
1177
245
  throw new Error(`No API key found for provider "${currentModel.provider}". Please set the appropriate environment variable or update ${getModelsPath()}`);
@@ -1180,44 +248,18 @@ export async function main(args) {
1180
248
  },
1181
249
  }),
1182
250
  });
1183
- // If initial thinking was requested but model doesn't support it, silently reset to off
251
+ // If initial thinking was requested but model doesn't support it, reset to off
1184
252
  if (initialThinking !== "off" && initialModel && !initialModel.reasoning) {
1185
253
  agent.setThinkingLevel("off");
1186
254
  }
1187
- // Track if we had to fall back from saved model (to show in chat later)
1188
- let modelFallbackMessage = null;
1189
- // Load previous messages if continuing or resuming
1190
- if (parsed.continue || parsed.resume) {
255
+ // Load previous messages if continuing, resuming, or using --session
256
+ if (parsed.continue || parsed.resume || parsed.session) {
1191
257
  const messages = sessionManager.loadMessages();
1192
258
  if (messages.length > 0) {
1193
259
  agent.replaceMessages(messages);
1194
260
  }
1195
- // Load and restore thinking level
1196
- const thinkingLevel = sessionManager.loadThinkingLevel();
1197
- if (thinkingLevel) {
1198
- agent.setThinkingLevel(thinkingLevel);
1199
- if (shouldPrintMessages) {
1200
- console.log(chalk.dim(`Restored thinking level: ${thinkingLevel}`));
1201
- }
1202
- }
1203
- // Check if we had to fall back from saved model
1204
- const savedModel = sessionManager.loadModel();
1205
- if (savedModel && initialModel) {
1206
- const savedMatches = initialModel.provider === savedModel.provider && initialModel.id === savedModel.modelId;
1207
- if (!savedMatches) {
1208
- const { model: restoredModel, error } = findModel(savedModel.provider, savedModel.modelId);
1209
- if (error) {
1210
- // Config error - already shown above, just use generic message
1211
- modelFallbackMessage = `Could not restore model ${savedModel.provider}/${savedModel.modelId}. Using ${initialModel.provider}/${initialModel.id}.`;
1212
- }
1213
- else {
1214
- const reason = !restoredModel ? "model no longer exists" : "no API key available";
1215
- modelFallbackMessage = `Could not restore model ${savedModel.provider}/${savedModel.modelId} (${reason}). Using ${initialModel.provider}/${initialModel.id}.`;
1216
- }
1217
- }
1218
- }
1219
261
  }
1220
- // Log loaded context files (they're already in the system prompt)
262
+ // Log loaded context files
1221
263
  if (shouldPrintMessages && !parsed.continue && !parsed.resume) {
1222
264
  const contextFiles = loadProjectContextFiles();
1223
265
  if (contextFiles.length > 0) {
@@ -1227,39 +269,25 @@ export async function main(args) {
1227
269
  }
1228
270
  }
1229
271
  }
272
+ // Load file commands for slash command expansion
273
+ const fileCommands = loadSlashCommands();
274
+ // Create session
275
+ const session = new AgentSession({
276
+ agent,
277
+ sessionManager,
278
+ settingsManager,
279
+ scopedModels,
280
+ fileCommands,
281
+ });
1230
282
  // Route to appropriate mode
1231
283
  if (mode === "rpc") {
1232
- // RPC mode - headless operation
1233
- await runRpcMode(agent, sessionManager, settingsManager);
284
+ await runRpcMode(session);
1234
285
  }
1235
286
  else if (isInteractive) {
1236
- // Check for new version in the background (don't block startup)
287
+ // Check for new version in the background
1237
288
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);
1238
- // Check if we should show changelog (only in interactive mode, only for new sessions)
1239
- let changelogMarkdown = null;
1240
- if (!parsed.continue && !parsed.resume) {
1241
- const lastVersion = settingsManager.getLastChangelogVersion();
1242
- // Check if we need to show changelog
1243
- if (!lastVersion) {
1244
- // First run - show all entries
1245
- const changelogPath = getChangelogPath();
1246
- const entries = parseChangelog(changelogPath);
1247
- if (entries.length > 0) {
1248
- changelogMarkdown = entries.map((e) => e.content).join("\n\n");
1249
- settingsManager.setLastChangelogVersion(VERSION);
1250
- }
1251
- }
1252
- else {
1253
- // Parse current and last versions
1254
- const changelogPath = getChangelogPath();
1255
- const entries = parseChangelog(changelogPath);
1256
- const newEntries = getNewEntries(entries, lastVersion);
1257
- if (newEntries.length > 0) {
1258
- changelogMarkdown = newEntries.map((e) => e.content).join("\n\n");
1259
- settingsManager.setLastChangelogVersion(VERSION);
1260
- }
1261
- }
1262
- }
289
+ // Check if we should show changelog
290
+ const changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);
1263
291
  // Show model scope if provided
1264
292
  if (scopedModels.length > 0) {
1265
293
  const modelList = scopedModels
@@ -1272,13 +300,79 @@ export async function main(args) {
1272
300
  }
1273
301
  // Ensure fd tool is available for file autocomplete
1274
302
  const fdPath = await ensureTool("fd");
1275
- // Interactive mode - use TUI (may have initial messages from CLI args)
1276
- const collapseChangelog = settingsManager.getCollapseChangelog();
1277
- await runInteractiveMode(agent, sessionManager, settingsManager, VERSION, changelogMarkdown, collapseChangelog, modelFallbackMessage, versionCheckPromise, scopedModels, parsed.messages, initialMessage, initialAttachments, fdPath);
303
+ await runInteractiveMode(session, VERSION, changelogMarkdown, modelFallbackMessage, versionCheckPromise, parsed.messages, initialMessage, initialAttachments, fdPath);
1278
304
  }
1279
305
  else {
1280
306
  // Non-interactive mode (--print flag or --mode flag)
1281
- await runSingleShotMode(agent, sessionManager, parsed.messages, mode, initialMessage, initialAttachments);
307
+ await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
1282
308
  }
1283
309
  }
310
+ /** Find initial model based on CLI args, scoped models, settings, or available models */
311
+ async function findInitialModelForSession(parsed, scopedModels, settingsManager) {
312
+ // 1. CLI args take priority
313
+ if (parsed.provider && parsed.model) {
314
+ const { model, error } = findModel(parsed.provider, parsed.model);
315
+ if (error) {
316
+ console.error(chalk.red(error));
317
+ process.exit(1);
318
+ }
319
+ if (!model) {
320
+ console.error(chalk.red(`Model ${parsed.provider}/${parsed.model} not found`));
321
+ process.exit(1);
322
+ }
323
+ return model;
324
+ }
325
+ // 2. Use first model from scoped models (skip if continuing/resuming)
326
+ if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
327
+ return scopedModels[0].model;
328
+ }
329
+ // 3. Try saved default from settings
330
+ const defaultProvider = settingsManager.getDefaultProvider();
331
+ const defaultModelId = settingsManager.getDefaultModel();
332
+ if (defaultProvider && defaultModelId) {
333
+ const { model, error } = findModel(defaultProvider, defaultModelId);
334
+ if (error) {
335
+ console.error(chalk.red(error));
336
+ process.exit(1);
337
+ }
338
+ if (model) {
339
+ return model;
340
+ }
341
+ }
342
+ // 4. Try first available model with valid API key
343
+ const { models: availableModels, error } = await getAvailableModels();
344
+ if (error) {
345
+ console.error(chalk.red(error));
346
+ process.exit(1);
347
+ }
348
+ if (availableModels.length > 0) {
349
+ return availableModels[0];
350
+ }
351
+ return null;
352
+ }
353
+ /** Get changelog markdown to display (only for new sessions with updates) */
354
+ function getChangelogForDisplay(parsed, settingsManager) {
355
+ if (parsed.continue || parsed.resume) {
356
+ return null;
357
+ }
358
+ const lastVersion = settingsManager.getLastChangelogVersion();
359
+ const changelogPath = getChangelogPath();
360
+ const entries = parseChangelog(changelogPath);
361
+ if (!lastVersion) {
362
+ // First run - show all entries
363
+ if (entries.length > 0) {
364
+ settingsManager.setLastChangelogVersion(VERSION);
365
+ return entries.map((e) => e.content).join("\n\n");
366
+ }
367
+ }
368
+ else {
369
+ // Check for new entries since last version
370
+ const newEntries = getNewEntries(entries, lastVersion);
371
+ if (newEntries.length > 0) {
372
+ settingsManager.setLastChangelogVersion(VERSION);
373
+ return newEntries.map((e) => e.content).join("\n\n");
374
+ }
375
+ }
376
+ return null;
377
+ }
1284
378
  //# sourceMappingURL=main.js.map