@prestyj/cli 4.2.15 → 4.2.44

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 (286) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -7
  3. package/dist/cli.js +457 -225
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +1 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +66 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +22 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +184 -23
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/agents.d.ts +0 -2
  14. package/dist/core/agents.d.ts.map +1 -1
  15. package/dist/core/agents.js +1 -5
  16. package/dist/core/agents.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts +2 -0
  18. package/dist/core/auth-storage.d.ts.map +1 -1
  19. package/dist/core/auth-storage.js +66 -17
  20. package/dist/core/auth-storage.js.map +1 -1
  21. package/dist/core/auto-update.js +1 -1
  22. package/dist/core/auto-update.js.map +1 -1
  23. package/dist/core/compaction/compactor.d.ts +36 -5
  24. package/dist/core/compaction/compactor.d.ts.map +1 -1
  25. package/dist/core/compaction/compactor.js +222 -63
  26. package/dist/core/compaction/compactor.js.map +1 -1
  27. package/dist/core/compaction/compactor.test.js +377 -32
  28. package/dist/core/compaction/compactor.test.js.map +1 -1
  29. package/dist/core/compaction/token-estimator.d.ts +5 -0
  30. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  31. package/dist/core/compaction/token-estimator.js +41 -2
  32. package/dist/core/compaction/token-estimator.js.map +1 -1
  33. package/dist/core/compaction/token-estimator.test.js +43 -21
  34. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  35. package/dist/core/event-bus.d.ts +6 -0
  36. package/dist/core/event-bus.d.ts.map +1 -1
  37. package/dist/core/event-bus.js +7 -0
  38. package/dist/core/event-bus.js.map +1 -1
  39. package/dist/core/file-lock.d.ts +6 -0
  40. package/dist/core/file-lock.d.ts.map +1 -0
  41. package/dist/core/file-lock.js +76 -0
  42. package/dist/core/file-lock.js.map +1 -0
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.d.ts.map +1 -1
  45. package/dist/core/index.js +1 -1
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/mcp/client.d.ts +0 -26
  48. package/dist/core/mcp/client.d.ts.map +1 -1
  49. package/dist/core/mcp/client.js +34 -141
  50. package/dist/core/mcp/client.js.map +1 -1
  51. package/dist/core/mcp/defaults.d.ts.map +1 -1
  52. package/dist/core/mcp/defaults.js +4 -8
  53. package/dist/core/mcp/defaults.js.map +1 -1
  54. package/dist/core/mcp/types.d.ts +0 -2
  55. package/dist/core/mcp/types.d.ts.map +1 -1
  56. package/dist/core/model-registry.d.ts +2 -1
  57. package/dist/core/model-registry.d.ts.map +1 -1
  58. package/dist/core/model-registry.js +16 -5
  59. package/dist/core/model-registry.js.map +1 -1
  60. package/dist/core/oauth/anthropic.d.ts.map +1 -1
  61. package/dist/core/oauth/anthropic.js +3 -3
  62. package/dist/core/oauth/anthropic.js.map +1 -1
  63. package/dist/core/prompt-commands.d.ts.map +1 -1
  64. package/dist/core/prompt-commands.js +21 -0
  65. package/dist/core/prompt-commands.js.map +1 -1
  66. package/dist/core/session-manager.d.ts +34 -2
  67. package/dist/core/session-manager.d.ts.map +1 -1
  68. package/dist/core/session-manager.js +160 -54
  69. package/dist/core/session-manager.js.map +1 -1
  70. package/dist/core/settings-manager.d.ts +2 -2
  71. package/dist/core/skills.d.ts +2 -1
  72. package/dist/core/skills.d.ts.map +1 -1
  73. package/dist/core/skills.js +8 -8
  74. package/dist/core/skills.js.map +1 -1
  75. package/dist/core/slash-commands.d.ts +4 -0
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +22 -0
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/telegram.d.ts +94 -0
  80. package/dist/core/telegram.d.ts.map +1 -0
  81. package/dist/core/telegram.js +227 -0
  82. package/dist/core/telegram.js.map +1 -0
  83. package/dist/interactive.d.ts.map +1 -1
  84. package/dist/interactive.js +36 -5
  85. package/dist/interactive.js.map +1 -1
  86. package/dist/modes/index.d.ts +1 -0
  87. package/dist/modes/index.d.ts.map +1 -1
  88. package/dist/modes/index.js +1 -0
  89. package/dist/modes/index.js.map +1 -1
  90. package/dist/modes/json-mode.d.ts.map +1 -1
  91. package/dist/modes/json-mode.js +2 -1
  92. package/dist/modes/json-mode.js.map +1 -1
  93. package/dist/modes/print-mode.d.ts.map +1 -1
  94. package/dist/modes/print-mode.js +2 -1
  95. package/dist/modes/print-mode.js.map +1 -1
  96. package/dist/modes/rpc-mode.d.ts +28 -0
  97. package/dist/modes/rpc-mode.d.ts.map +1 -0
  98. package/dist/modes/rpc-mode.js +146 -0
  99. package/dist/modes/rpc-mode.js.map +1 -0
  100. package/dist/modes/serve-mode.d.ts +21 -0
  101. package/dist/modes/serve-mode.d.ts.map +1 -0
  102. package/dist/modes/serve-mode.js +650 -0
  103. package/dist/modes/serve-mode.js.map +1 -0
  104. package/dist/system-prompt.d.ts +1 -1
  105. package/dist/system-prompt.d.ts.map +1 -1
  106. package/dist/system-prompt.js +45 -10
  107. package/dist/system-prompt.js.map +1 -1
  108. package/dist/tools/bash.d.ts +4 -1
  109. package/dist/tools/bash.d.ts.map +1 -1
  110. package/dist/tools/bash.js +58 -5
  111. package/dist/tools/bash.js.map +1 -1
  112. package/dist/tools/edit.d.ts +4 -1
  113. package/dist/tools/edit.d.ts.map +1 -1
  114. package/dist/tools/edit.js +9 -5
  115. package/dist/tools/edit.js.map +1 -1
  116. package/dist/tools/enter-plan.d.ts +8 -0
  117. package/dist/tools/enter-plan.d.ts.map +1 -0
  118. package/dist/tools/enter-plan.js +28 -0
  119. package/dist/tools/enter-plan.js.map +1 -0
  120. package/dist/tools/exit-plan.d.ts +8 -0
  121. package/dist/tools/exit-plan.d.ts.map +1 -0
  122. package/dist/tools/exit-plan.js +35 -0
  123. package/dist/tools/exit-plan.js.map +1 -0
  124. package/dist/tools/grep.d.ts +2 -1
  125. package/dist/tools/grep.d.ts.map +1 -1
  126. package/dist/tools/grep.js +7 -8
  127. package/dist/tools/grep.js.map +1 -1
  128. package/dist/tools/index.d.ts +17 -0
  129. package/dist/tools/index.d.ts.map +1 -1
  130. package/dist/tools/index.js +26 -7
  131. package/dist/tools/index.js.map +1 -1
  132. package/dist/tools/ls.d.ts +2 -1
  133. package/dist/tools/ls.d.ts.map +1 -1
  134. package/dist/tools/ls.js +4 -4
  135. package/dist/tools/ls.js.map +1 -1
  136. package/dist/tools/operations.d.ts +39 -0
  137. package/dist/tools/operations.d.ts.map +1 -0
  138. package/dist/tools/operations.js +27 -0
  139. package/dist/tools/operations.js.map +1 -0
  140. package/dist/tools/path-utils.d.ts +5 -0
  141. package/dist/tools/path-utils.d.ts.map +1 -1
  142. package/dist/tools/path-utils.js +18 -0
  143. package/dist/tools/path-utils.js.map +1 -1
  144. package/dist/tools/plan-mode.test.d.ts +2 -0
  145. package/dist/tools/plan-mode.test.d.ts.map +1 -0
  146. package/dist/tools/plan-mode.test.js +214 -0
  147. package/dist/tools/plan-mode.test.js.map +1 -0
  148. package/dist/tools/read.d.ts +2 -1
  149. package/dist/tools/read.d.ts.map +1 -1
  150. package/dist/tools/read.js +6 -5
  151. package/dist/tools/read.js.map +1 -1
  152. package/dist/tools/skill.d.ts +10 -0
  153. package/dist/tools/skill.d.ts.map +1 -0
  154. package/dist/tools/skill.js +36 -0
  155. package/dist/tools/skill.js.map +1 -0
  156. package/dist/tools/subagent.d.ts +3 -1
  157. package/dist/tools/subagent.d.ts.map +1 -1
  158. package/dist/tools/subagent.js +46 -4
  159. package/dist/tools/subagent.js.map +1 -1
  160. package/dist/tools/tasks.d.ts +2 -2
  161. package/dist/tools/web-fetch.d.ts.map +1 -1
  162. package/dist/tools/web-fetch.js +47 -0
  163. package/dist/tools/web-fetch.js.map +1 -1
  164. package/dist/tools/write.d.ts +4 -1
  165. package/dist/tools/write.d.ts.map +1 -1
  166. package/dist/tools/write.js +17 -6
  167. package/dist/tools/write.js.map +1 -1
  168. package/dist/ui/App.d.ts +35 -1
  169. package/dist/ui/App.d.ts.map +1 -1
  170. package/dist/ui/App.js +367 -73
  171. package/dist/ui/App.js.map +1 -1
  172. package/dist/ui/components/ActivityIndicator.d.ts +2 -1
  173. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  174. package/dist/ui/components/ActivityIndicator.js +25 -4
  175. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  176. package/dist/ui/components/AnimationContext.d.ts +22 -0
  177. package/dist/ui/components/AnimationContext.d.ts.map +1 -0
  178. package/dist/ui/components/AnimationContext.js +35 -0
  179. package/dist/ui/components/AnimationContext.js.map +1 -0
  180. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  181. package/dist/ui/components/AssistantMessage.js +6 -1
  182. package/dist/ui/components/AssistantMessage.js.map +1 -1
  183. package/dist/ui/components/Banner.d.ts.map +1 -1
  184. package/dist/ui/components/Banner.js +4 -7
  185. package/dist/ui/components/Banner.js.map +1 -1
  186. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  187. package/dist/ui/components/CompactionNotice.js +4 -8
  188. package/dist/ui/components/CompactionNotice.js.map +1 -1
  189. package/dist/ui/components/Footer.d.ts +2 -1
  190. package/dist/ui/components/Footer.d.ts.map +1 -1
  191. package/dist/ui/components/Footer.js +8 -15
  192. package/dist/ui/components/Footer.js.map +1 -1
  193. package/dist/ui/components/InputArea.d.ts +3 -1
  194. package/dist/ui/components/InputArea.d.ts.map +1 -1
  195. package/dist/ui/components/InputArea.js +23 -25
  196. package/dist/ui/components/InputArea.js.map +1 -1
  197. package/dist/ui/components/PlanApproval.d.ts +8 -0
  198. package/dist/ui/components/PlanApproval.d.ts.map +1 -0
  199. package/dist/ui/components/PlanApproval.js +44 -0
  200. package/dist/ui/components/PlanApproval.js.map +1 -0
  201. package/dist/ui/components/PlanBanner.d.ts +6 -0
  202. package/dist/ui/components/PlanBanner.d.ts.map +1 -0
  203. package/dist/ui/components/PlanBanner.js +14 -0
  204. package/dist/ui/components/PlanBanner.js.map +1 -0
  205. package/dist/ui/components/PlanOverlay.d.ts +11 -0
  206. package/dist/ui/components/PlanOverlay.d.ts.map +1 -0
  207. package/dist/ui/components/PlanOverlay.js +263 -0
  208. package/dist/ui/components/PlanOverlay.js.map +1 -0
  209. package/dist/ui/components/ServerToolExecution.d.ts +1 -0
  210. package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
  211. package/dist/ui/components/ServerToolExecution.js +12 -3
  212. package/dist/ui/components/ServerToolExecution.js.map +1 -1
  213. package/dist/ui/components/SkillsOverlay.d.ts +7 -0
  214. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -0
  215. package/dist/ui/components/SkillsOverlay.js +158 -0
  216. package/dist/ui/components/SkillsOverlay.js.map +1 -0
  217. package/dist/ui/components/Spinner.d.ts.map +1 -1
  218. package/dist/ui/components/Spinner.js +5 -9
  219. package/dist/ui/components/Spinner.js.map +1 -1
  220. package/dist/ui/components/StreamingArea.d.ts +2 -1
  221. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  222. package/dist/ui/components/StreamingArea.js +11 -4
  223. package/dist/ui/components/StreamingArea.js.map +1 -1
  224. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  225. package/dist/ui/components/SubAgentPanel.js +27 -29
  226. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  227. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  228. package/dist/ui/components/TaskOverlay.js +7 -3
  229. package/dist/ui/components/TaskOverlay.js.map +1 -1
  230. package/dist/ui/components/ThinkingBlock.d.ts.map +1 -1
  231. package/dist/ui/components/ThinkingBlock.js +4 -1
  232. package/dist/ui/components/ThinkingBlock.js.map +1 -1
  233. package/dist/ui/components/ThinkingIndicator.d.ts.map +1 -1
  234. package/dist/ui/components/ThinkingIndicator.js +8 -33
  235. package/dist/ui/components/ThinkingIndicator.js.map +1 -1
  236. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  237. package/dist/ui/components/ToolExecution.js +95 -41
  238. package/dist/ui/components/ToolExecution.js.map +1 -1
  239. package/dist/ui/components/ToolGroupExecution.d.ts +7 -0
  240. package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -0
  241. package/dist/ui/components/ToolGroupExecution.js +115 -0
  242. package/dist/ui/components/ToolGroupExecution.js.map +1 -0
  243. package/dist/ui/components/UserMessage.js +1 -1
  244. package/dist/ui/components/UserMessage.js.map +1 -1
  245. package/dist/ui/components/index.d.ts +1 -0
  246. package/dist/ui/components/index.d.ts.map +1 -1
  247. package/dist/ui/components/index.js +1 -0
  248. package/dist/ui/components/index.js.map +1 -1
  249. package/dist/ui/hooks/useAgentLoop.d.ts +8 -0
  250. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  251. package/dist/ui/hooks/useAgentLoop.js +320 -220
  252. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  253. package/dist/ui/hooks/useTerminalSize.d.ts +21 -9
  254. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  255. package/dist/ui/hooks/useTerminalSize.js +33 -16
  256. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  257. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  258. package/dist/ui/hooks/useTerminalTitle.js +8 -16
  259. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  260. package/dist/ui/live-item-flush.d.ts +11 -1
  261. package/dist/ui/live-item-flush.d.ts.map +1 -1
  262. package/dist/ui/live-item-flush.js +81 -2
  263. package/dist/ui/live-item-flush.js.map +1 -1
  264. package/dist/ui/login.d.ts +1 -1
  265. package/dist/ui/login.d.ts.map +1 -1
  266. package/dist/ui/login.js +30 -6
  267. package/dist/ui/login.js.map +1 -1
  268. package/dist/ui/render.d.ts +11 -0
  269. package/dist/ui/render.d.ts.map +1 -1
  270. package/dist/ui/render.js +24 -10
  271. package/dist/ui/render.js.map +1 -1
  272. package/dist/ui/sessions.d.ts +2 -0
  273. package/dist/ui/sessions.d.ts.map +1 -0
  274. package/dist/ui/sessions.js +208 -0
  275. package/dist/ui/sessions.js.map +1 -0
  276. package/dist/ui/theme/dark.json +3 -1
  277. package/dist/ui/theme/light.json +3 -1
  278. package/dist/ui/theme/theme.d.ts +2 -0
  279. package/dist/ui/theme/theme.d.ts.map +1 -1
  280. package/dist/utils/error-handler.d.ts.map +1 -1
  281. package/dist/utils/error-handler.js +17 -5
  282. package/dist/utils/error-handler.js.map +1 -1
  283. package/dist/utils/image.d.ts.map +1 -1
  284. package/dist/utils/image.js +60 -5
  285. package/dist/utils/image.js.map +1 -1
  286. package/package.json +9 -9
package/dist/cli.js CHANGED
@@ -1,16 +1,39 @@
1
1
  #!/usr/bin/env node
2
+ // Catch stray abort-related promise rejections that escape the normal error
3
+ // handling chain (e.g. race conditions during Ctrl+C). Without this, Node.js
4
+ // v25+ crashes the process on any unhandled rejection.
5
+ process.on("unhandledRejection", (reason) => {
6
+ if (reason instanceof Error) {
7
+ const msg = reason.message.toLowerCase();
8
+ if (reason.name === "AbortError" || msg.includes("aborted") || msg.includes("abort")) {
9
+ // Silently swallow abort rejections — these are expected during cancellation
10
+ return;
11
+ }
12
+ }
13
+ // Re-throw non-abort rejections so they still crash with a useful stack trace
14
+ throw reason;
15
+ });
2
16
  // Drain performance entries to prevent buffer overflow warning from dependencies
3
- import { PerformanceObserver } from "node:perf_hooks";
4
- new PerformanceObserver(() => { }).observe({ entryTypes: ["measure", "mark"] });
17
+ import { PerformanceObserver, performance } from "node:perf_hooks";
18
+ new PerformanceObserver((list) => {
19
+ for (const entry of list.getEntries()) {
20
+ if (entry.entryType === "measure")
21
+ performance.clearMeasures(entry.name);
22
+ else if (entry.entryType === "mark")
23
+ performance.clearMarks(entry.name);
24
+ }
25
+ }).observe({ entryTypes: ["measure", "mark"] });
5
26
  import { parseArgs } from "node:util";
6
27
  import fs from "node:fs";
7
28
  import readline from "node:readline/promises";
8
29
  import { execFile } from "node:child_process";
9
30
  import { createRequire } from "node:module";
10
- import { runPrintMode } from "./modes/print-mode.js";
11
- import { runJsonMode } from "./modes/json-mode.js";
12
31
  import { renderApp } from "./ui/render.js";
32
+ import { runJsonMode } from "./modes/json-mode.js";
33
+ import { runRpcMode } from "./modes/rpc-mode.js";
34
+ import { runServeMode } from "./modes/serve-mode.js";
13
35
  import { renderLoginSelector } from "./ui/login.js";
36
+ import { renderSessionSelector } from "./ui/sessions.js";
14
37
  import { formatUserError } from "./utils/error-handler.js";
15
38
  import { AuthStorage } from "./core/auth-storage.js";
16
39
  import { SessionManager } from "./core/session-manager.js";
@@ -18,52 +41,19 @@ import { ensureAppDirs, getAppPaths } from "./config.js";
18
41
  import { initLogger, log, closeLogger } from "./core/logger.js";
19
42
  import { buildSystemPrompt } from "./system-prompt.js";
20
43
  import { createTools } from "./tools/index.js";
44
+ import { shouldCompact, compact } from "./core/compaction/compactor.js";
45
+ import { setEstimatorModel } from "./core/compaction/token-estimator.js";
46
+ import { getContextWindow } from "./core/model-registry.js";
21
47
  import { MCPClientManager, getMCPServers } from "./core/mcp/index.js";
22
48
  import { discoverAgents } from "./core/agents.js";
49
+ import { discoverSkills } from "./core/skills.js";
50
+ import path from "node:path";
23
51
  import { loginAnthropic } from "./core/oauth/anthropic.js";
24
52
  import { loginOpenAI } from "./core/oauth/openai.js";
25
53
  import chalk from "chalk";
26
54
  import { checkAndAutoUpdate } from "./core/auto-update.js";
27
55
  const _require = createRequire(import.meta.url);
28
56
  const CLI_VERSION = _require("../package.json").version;
29
- const USAGE = `
30
- Usage: ezcoder [command] [options] [message...]
31
-
32
- Commands:
33
- login Log in to a provider via OAuth
34
- logout Log out (clear stored credentials)
35
- continue Resume the most recent session for this directory
36
-
37
- Options:
38
- -p, --provider <name> LLM provider (anthropic, openai, glm, moonshot) [default: anthropic]
39
- -m, --model <name> Model name [default: claude-opus-4-6]
40
- --base-url <url> Custom API base URL
41
- --system-prompt <text> Override system prompt
42
- --thinking <level> Thinking level (low, medium, high, max)
43
- --max-turns <n> Maximum agent loop turns [default: 40]
44
- -s, --session <path> Resume a specific session file
45
- --print Print mode: one-shot, output to stdout, then exit
46
- --json JSON mode: one-shot, output NDJSON events to stdout
47
- -v, --version Show version number
48
- -h, --help Show this help message
49
-
50
- Print mode:
51
- echo "hello" | ezcoder --print
52
- ezcoder --print "explain this code"
53
-
54
- Authentication:
55
- ezcoder login Log in (select provider interactively)
56
- ezcoder logout Log out from all providers
57
- `.trim();
58
- function getHardcodedDefault(p) {
59
- if (p === "openai")
60
- return "gpt-5.3-codex";
61
- if (p === "glm")
62
- return "glm-5";
63
- if (p === "moonshot")
64
- return "kimi-k2.5";
65
- return "claude-opus-4-6";
66
- }
67
57
  function main() {
68
58
  // Silent auto-update check (throttled, non-blocking on failure)
69
59
  const updateMessage = checkAndAutoUpdate(CLI_VERSION);
@@ -90,23 +80,48 @@ function main() {
90
80
  });
91
81
  return;
92
82
  }
83
+ if (subcommand === "sessions") {
84
+ process.argv.splice(2, 1);
85
+ runSessions().catch((err) => {
86
+ log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
87
+ closeLogger();
88
+ process.stderr.write(formatUserError(err) + "\n");
89
+ process.exit(1);
90
+ });
91
+ return;
92
+ }
93
+ if (subcommand === "telegram") {
94
+ runTelegramSetup().catch((err) => {
95
+ log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
96
+ closeLogger();
97
+ process.stderr.write(formatUserError(err) + "\n");
98
+ process.exit(1);
99
+ });
100
+ return;
101
+ }
102
+ if (subcommand === "serve") {
103
+ process.argv.splice(2, 1);
104
+ runServe().catch((err) => {
105
+ log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
106
+ closeLogger();
107
+ process.stderr.write(formatUserError(err) + "\n");
108
+ process.exit(1);
109
+ });
110
+ return;
111
+ }
93
112
  if (subcommand === "continue") {
94
113
  // Remove "continue" so parseArgs handles remaining flags
95
114
  process.argv.splice(2, 1);
96
115
  }
97
116
  const { values, positionals } = parseArgs({
98
117
  options: {
99
- provider: { type: "string", short: "p" },
100
- model: { type: "string", short: "m" },
101
- "base-url": { type: "string" },
102
- "system-prompt": { type: "string" },
103
- thinking: { type: "string" },
104
- "max-turns": { type: "string" },
105
- session: { type: "string", short: "s" },
106
- print: { type: "boolean" },
107
- json: { type: "boolean" },
108
118
  version: { type: "boolean", short: "v" },
109
- help: { type: "boolean", short: "h" },
119
+ json: { type: "boolean" },
120
+ rpc: { type: "boolean" },
121
+ provider: { type: "string" },
122
+ model: { type: "string" },
123
+ "max-turns": { type: "string" },
124
+ "system-prompt": { type: "string" },
110
125
  },
111
126
  allowPositionals: true,
112
127
  strict: true,
@@ -115,9 +130,43 @@ function main() {
115
130
  console.log(CLI_VERSION);
116
131
  process.exit(0);
117
132
  }
118
- if (values.help) {
119
- console.log(USAGE);
120
- process.exit(0);
133
+ // JSON mode — used by sub-agents
134
+ if (values.json) {
135
+ const message = positionals[0] ?? "";
136
+ const jsonProvider = (values.provider ?? "anthropic");
137
+ const jsonModel = values.model ?? "claude-opus-4-6";
138
+ const maxTurns = values["max-turns"] ? parseInt(values["max-turns"], 10) : undefined;
139
+ const systemPrompt = values["system-prompt"];
140
+ const cwd = process.cwd();
141
+ runJsonMode({
142
+ message,
143
+ provider: jsonProvider,
144
+ model: jsonModel,
145
+ cwd,
146
+ systemPrompt,
147
+ maxTurns,
148
+ }).catch((err) => {
149
+ process.stderr.write(formatUserError(err) + "\n");
150
+ process.exit(1);
151
+ });
152
+ return;
153
+ }
154
+ // RPC mode — headless JSON-over-stdio for IDE integrations
155
+ if (values.rpc) {
156
+ const rpcProvider = (values.provider ?? "anthropic");
157
+ const rpcModel = values.model ?? "claude-opus-4-6";
158
+ const systemPrompt = values["system-prompt"];
159
+ const cwd = process.cwd();
160
+ runRpcMode({
161
+ provider: rpcProvider,
162
+ model: rpcModel,
163
+ cwd,
164
+ systemPrompt,
165
+ }).catch((err) => {
166
+ process.stderr.write(formatUserError(err) + "\n");
167
+ process.exit(1);
168
+ });
169
+ return;
121
170
  }
122
171
  // Load saved settings for model/provider persistence
123
172
  let savedProvider;
@@ -138,74 +187,27 @@ function main() {
138
187
  catch {
139
188
  // No settings file or invalid JSON — use defaults
140
189
  }
141
- // Priority: CLI flag > saved setting > hardcoded default
142
- const providerFromFlag = values.provider;
143
- const provider = providerFromFlag ?? savedProvider ?? "anthropic";
144
- const defaultModel = getHardcodedDefault(provider);
145
- // Use saved model only if provider matches (or no provider flag was given)
146
- const model = values.model ?? (!providerFromFlag && savedModel ? savedModel : defaultModel);
147
- // CLI --thinking flag overrides saved setting; saved thinkingEnabled provides default
148
- const thinkingLevel = values.thinking ?? (savedThinkingEnabled ? "medium" : undefined);
149
- const maxTurns = values["max-turns"] ? parseInt(values["max-turns"], 10) : undefined;
150
- // Print mode
151
- if (values.print) {
152
- const message = positionals.join(" ").trim() || readStdinSync();
153
- if (!message) {
154
- console.error("Error: --print requires a message (positional args or stdin)");
155
- process.exit(1);
156
- }
157
- runPrintMode({
158
- message,
159
- provider,
160
- model,
161
- baseUrl: values["base-url"],
162
- systemPrompt: values["system-prompt"],
163
- cwd: process.cwd(),
164
- thinkingLevel,
165
- }).catch((err) => {
166
- log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
167
- closeLogger();
168
- process.stderr.write(formatUserError(err) + "\n");
169
- process.exit(1);
170
- });
171
- return;
172
- }
173
- // JSON mode
174
- if (values.json) {
175
- const message = positionals.join(" ").trim() || readStdinSync();
176
- if (!message) {
177
- console.error("Error: --json requires a message (positional args or stdin)");
178
- process.exit(1);
179
- }
180
- runJsonMode({
181
- message,
182
- provider,
183
- model,
184
- baseUrl: values["base-url"],
185
- systemPrompt: values["system-prompt"],
186
- cwd: process.cwd(),
187
- thinkingLevel,
188
- maxTurns,
189
- }).catch((err) => {
190
- log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
191
- closeLogger();
192
- process.stderr.write(formatUserError(err) + "\n");
193
- process.exit(1);
194
- });
195
- return;
190
+ const provider = savedProvider ?? "anthropic";
191
+ function getHardcodedDefault(p) {
192
+ if (p === "openai")
193
+ return "gpt-5.3-codex";
194
+ if (p === "glm")
195
+ return "glm-5";
196
+ if (p === "moonshot")
197
+ return "kimi-k2.5";
198
+ return "claude-opus-4-6";
196
199
  }
200
+ const model = savedModel ?? getHardcodedDefault(provider);
201
+ const thinkingLevel = savedThinkingEnabled ? "medium" : undefined;
197
202
  // Interactive mode (Ink TUI)
198
203
  const cwd = process.cwd();
199
204
  const continueRecent = subcommand === "continue";
200
205
  runInkTUI({
201
206
  provider,
202
207
  model,
203
- baseUrl: values["base-url"],
204
208
  cwd,
205
209
  thinkingLevel,
206
- systemPrompt: values["system-prompt"],
207
210
  continueRecent,
208
- sessionPath: values.session,
209
211
  theme: savedTheme,
210
212
  }).catch((err) => {
211
213
  log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
@@ -216,42 +218,20 @@ function main() {
216
218
  }
217
219
  // ── Ink TUI ───────────────────────────────────────────────
218
220
  async function runInkTUI(opts) {
219
- const { cwd } = opts;
220
- let provider = opts.provider;
221
- let model = opts.model;
221
+ const { provider, model, cwd } = opts;
222
+ // Set model for token estimation accuracy
223
+ setEstimatorModel(model);
222
224
  // Resolve auth
223
225
  const paths = await ensureAppDirs();
224
- const authStorage = new AuthStorage(paths.authFile);
225
- await authStorage.load();
226
- let creds;
227
- try {
228
- creds = await authStorage.resolveCredentials(provider);
229
- }
230
- catch {
231
- // Not logged in — let user pick a provider and log in
232
- const picked = await renderLoginSelector();
233
- if (!picked) {
234
- console.log(chalk.hex("#6b7280")("Login cancelled."));
235
- process.exit(0);
236
- }
237
- provider = picked;
238
- // Update model to match the picked provider's default
239
- model = getHardcodedDefault(provider);
240
- // Check if already logged into the picked provider
241
- try {
242
- creds = await authStorage.resolveCredentials(provider);
243
- }
244
- catch {
245
- console.log(chalk.hex("#60a5fa")(`\nLogging in to ${displayName(provider)}...\n`));
246
- creds = await runInlineLogin(authStorage, provider);
247
- }
248
- }
249
226
  initLogger(paths.logFile, {
250
227
  version: CLI_VERSION,
251
228
  provider,
252
229
  model,
253
230
  thinking: opts.thinkingLevel,
254
231
  });
232
+ const authStorage = new AuthStorage(paths.authFile);
233
+ await authStorage.load();
234
+ const creds = await authStorage.resolveCredentials(provider);
255
235
  // Detect all logged-in providers and preload their credentials
256
236
  const allProviders = ["anthropic", "openai", "glm", "moonshot"];
257
237
  const loggedInProviders = [];
@@ -272,24 +252,45 @@ async function runInkTUI(opts) {
272
252
  }
273
253
  }
274
254
  }
275
- // Discover agents and build tools
255
+ // Ensure project-local .ezcoder directories exist
256
+ const localGGDir = path.join(cwd, ".ezcoder");
257
+ await fs.promises.mkdir(path.join(localGGDir, "skills"), { recursive: true });
258
+ await fs.promises.mkdir(path.join(localGGDir, "commands"), { recursive: true });
259
+ await fs.promises.mkdir(path.join(localGGDir, "agents"), { recursive: true });
260
+ // Discover agents and skills
276
261
  const agents = await discoverAgents({
277
262
  globalAgentsDir: paths.agentsDir,
278
263
  projectDir: cwd,
279
264
  });
265
+ const skills = await discoverSkills({
266
+ globalSkillsDir: paths.skillsDir,
267
+ projectDir: cwd,
268
+ });
280
269
  // Build system prompt & tools (with sub-agent support)
281
- const systemPrompt = opts.systemPrompt ?? (await buildSystemPrompt(cwd));
282
- const { tools, processManager } = createTools(cwd, { agents, provider, model });
283
- // Connect MCP servers (lazy servers connect on first tool use)
270
+ const systemPrompt = await buildSystemPrompt(cwd, skills);
271
+ // Plan mode refs shared between tools and UI
272
+ const planModeRef = { current: false };
273
+ const onEnterPlanRef = {
274
+ current: () => { },
275
+ };
276
+ const onExitPlanRef = {
277
+ current: () => Promise.resolve("cancelled"),
278
+ };
279
+ const { tools, processManager } = createTools(cwd, {
280
+ agents,
281
+ skills,
282
+ provider,
283
+ model,
284
+ planModeRef,
285
+ onEnterPlan: (reason) => onEnterPlanRef.current(reason),
286
+ onExitPlan: (planPath) => onExitPlanRef.current(planPath),
287
+ });
288
+ // Connect MCP servers
284
289
  const mcpManager = new MCPClientManager();
285
290
  try {
286
291
  const providerApiKey = provider === "glm" ? credentialsByProvider["glm"]?.accessToken : undefined;
287
- const allConfigs = getMCPServers(provider, providerApiKey);
288
- const eagerConfigs = allConfigs.filter((c) => !c.lazy);
289
- const lazyConfigs = allConfigs.filter((c) => c.lazy);
290
- const mcpTools = await mcpManager.connectAll(eagerConfigs);
291
- const lazyTools = mcpManager.registerLazy(lazyConfigs);
292
- tools.push(...mcpTools, ...lazyTools);
292
+ const mcpTools = await mcpManager.connectAll(getMCPServers(provider, providerApiKey));
293
+ tools.push(...mcpTools);
293
294
  }
294
295
  catch (err) {
295
296
  log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -305,31 +306,50 @@ async function runInkTUI(opts) {
305
306
  const sessionManager = new SessionManager(paths.sessionsDir);
306
307
  let sessionPath;
307
308
  let initialHistory;
308
- if (opts.continueRecent || opts.sessionPath) {
309
- const existingPath = opts.sessionPath ?? (await sessionManager.getMostRecent(cwd));
310
- if (existingPath) {
311
- try {
312
- const loaded = await sessionManager.load(existingPath);
313
- const loadedMessages = sessionManager.getMessages(loaded.entries);
314
- if (loadedMessages.length > 0) {
315
- messages.push(...loadedMessages);
316
- sessionPath = existingPath;
317
- log("INFO", "session", `Restored session`, {
318
- path: existingPath,
319
- messageCount: String(loadedMessages.length),
309
+ // Determine which session to resume (explicit path or most recent)
310
+ const resumePath = opts.resumeSessionPath ??
311
+ (opts.continueRecent ? await sessionManager.getMostRecent(cwd) : null);
312
+ if (resumePath) {
313
+ try {
314
+ const loaded = await sessionManager.load(resumePath);
315
+ const loadedMessages = sessionManager.getMessages(loaded.entries);
316
+ if (loadedMessages.length > 0) {
317
+ messages.push(...loadedMessages);
318
+ sessionPath = resumePath;
319
+ log("INFO", "session", `Restored session`, {
320
+ path: resumePath,
321
+ messageCount: String(loadedMessages.length),
322
+ });
323
+ // Auto-compact on load if the restored session exceeds the context window.
324
+ // Without this, huge sessions (1M+ tokens) get loaded into memory and OOM.
325
+ const contextWindow = getContextWindow(model);
326
+ if (shouldCompact(messages, contextWindow, 0.8)) {
327
+ log("INFO", "session", `Restored session exceeds context — auto-compacting`);
328
+ const compacted = await compact(messages, {
329
+ provider,
330
+ model,
331
+ apiKey: creds.accessToken,
332
+ contextWindow,
320
333
  });
321
- initialHistory = messagesToHistoryItems(loadedMessages);
322
- initialHistory.push({
323
- kind: "info",
324
- text: `↻ Restored session (${loadedMessages.length} messages)`,
325
- id: `restore-info`,
334
+ // Replace messages array contents with compacted messages
335
+ messages.length = 0;
336
+ messages.push(...compacted.messages);
337
+ log("INFO", "session", `Auto-compaction complete`, {
338
+ before: String(compacted.result.originalCount),
339
+ after: String(compacted.result.newCount),
326
340
  });
327
341
  }
328
- }
329
- catch {
330
- // Session file corrupt or missing — start fresh
342
+ initialHistory = messagesToHistoryItems(loadedMessages);
343
+ initialHistory.push({
344
+ kind: "info",
345
+ text: `↻ Restored session (${loadedMessages.length} messages)`,
346
+ id: `restore-info`,
347
+ });
331
348
  }
332
349
  }
350
+ catch {
351
+ // Session file corrupt or missing — start fresh
352
+ }
333
353
  }
334
354
  // Create a new session file if we didn't reuse one
335
355
  if (!sessionPath) {
@@ -347,7 +367,6 @@ async function runInkTUI(opts) {
347
367
  maxTokens: 16384,
348
368
  thinking: opts.thinkingLevel,
349
369
  apiKey: creds.accessToken,
350
- baseUrl: opts.baseUrl,
351
370
  accountId: creds.accountId,
352
371
  cwd,
353
372
  theme: opts.theme,
@@ -360,18 +379,23 @@ async function runInkTUI(opts) {
360
379
  settingsFile: paths.settingsFile,
361
380
  mcpManager,
362
381
  authStorage,
382
+ planModeRef,
383
+ onEnterPlanRef,
384
+ onExitPlanRef,
385
+ skills,
363
386
  });
364
387
  closeLogger();
365
388
  }
366
389
  // ── Login ──────────────────────────────────────────────────
367
390
  async function runLogin() {
391
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
368
392
  const paths = await ensureAppDirs();
369
393
  initLogger(paths.logFile, { version: CLI_VERSION });
370
394
  log("INFO", "auth", "Login flow started");
371
395
  const authStorage = new AuthStorage();
372
396
  await authStorage.load();
373
397
  // Phase 1: Ink-based provider selector
374
- const provider = await renderLoginSelector();
398
+ const provider = await renderLoginSelector(CLI_VERSION);
375
399
  if (!provider) {
376
400
  console.log(chalk.hex("#6b7280")("Login cancelled."));
377
401
  return;
@@ -424,52 +448,6 @@ async function runLogin() {
424
448
  closeLogger();
425
449
  }
426
450
  }
427
- // ── Inline Login (auto-triggered when not logged in) ──────
428
- async function runInlineLogin(authStorage, provider) {
429
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
430
- try {
431
- const callbacks = {
432
- onOpenUrl: (url) => {
433
- console.log(chalk.hex("#60a5fa").bold("Opening browser..."));
434
- openBrowser(url);
435
- console.log(chalk.hex("#6b7280")("\nIf the browser didn't open, visit:\n") +
436
- chalk.hex("#6b7280")(url) +
437
- "\n");
438
- },
439
- onPromptCode: async (message) => {
440
- return rl.question(message + " ");
441
- },
442
- onStatus: (message) => {
443
- console.log(chalk.hex("#6b7280")(message));
444
- },
445
- };
446
- let creds;
447
- if (provider === "glm" || provider === "moonshot") {
448
- const keyLabel = provider === "glm" ? "Z.AI" : "Moonshot";
449
- const apiKey = await rl.question(chalk.hex("#60a5fa")(`Paste your ${keyLabel} API key: `));
450
- if (!apiKey.trim()) {
451
- console.error(chalk.hex("#ef4444")("No API key provided."));
452
- process.exit(1);
453
- }
454
- creds = {
455
- accessToken: apiKey.trim(),
456
- refreshToken: "",
457
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000 * 100,
458
- };
459
- }
460
- else {
461
- creds =
462
- provider === "anthropic" ? await loginAnthropic(callbacks) : await loginOpenAI(callbacks);
463
- }
464
- await authStorage.setCredentials(provider, creds);
465
- log("INFO", "auth", `Inline login succeeded for ${displayName(provider)}`);
466
- console.log(chalk.hex("#4ade80")(`✓ Logged in to ${displayName(provider)}!\n`));
467
- return creds;
468
- }
469
- finally {
470
- rl.close();
471
- }
472
- }
473
451
  // ── Logout ─────────────────────────────────────────────────
474
452
  async function runLogout() {
475
453
  const paths = await ensureAppDirs();
@@ -482,17 +460,271 @@ async function runLogout() {
482
460
  closeLogger();
483
461
  console.log(chalk.green("Logged out successfully."));
484
462
  }
485
- // ── Helpers ────────────────────────────────────────────────
486
- function readStdinSync() {
487
- if (process.stdin.isTTY)
488
- return "";
463
+ // ── Sessions ──────────────────────────────────────────────
464
+ async function runSessions() {
465
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
466
+ const paths = await ensureAppDirs();
467
+ initLogger(paths.logFile, { version: CLI_VERSION });
468
+ log("INFO", "session", "Sessions selector started");
469
+ const cwd = process.cwd();
470
+ const selectedPath = await renderSessionSelector(paths.sessionsDir, cwd, CLI_VERSION);
471
+ if (!selectedPath) {
472
+ console.log(chalk.hex("#6b7280")("No session selected."));
473
+ closeLogger();
474
+ process.exit(0);
475
+ }
476
+ // Load saved settings for provider/model/theme
477
+ let savedProvider;
478
+ let savedModel;
479
+ let savedThinkingEnabled = false;
480
+ let savedTheme = "auto";
481
+ try {
482
+ const raw = JSON.parse(fs.readFileSync(paths.settingsFile, "utf-8"));
483
+ if (raw.defaultProvider)
484
+ savedProvider = raw.defaultProvider;
485
+ if (raw.defaultModel)
486
+ savedModel = raw.defaultModel;
487
+ if (raw.thinkingEnabled === true)
488
+ savedThinkingEnabled = true;
489
+ if (raw.theme === "dark" || raw.theme === "light" || raw.theme === "auto")
490
+ savedTheme = raw.theme;
491
+ }
492
+ catch {
493
+ // No settings file — use defaults
494
+ }
495
+ const provider = savedProvider ?? "anthropic";
496
+ function getDefault(p) {
497
+ if (p === "openai")
498
+ return "gpt-5.3-codex";
499
+ if (p === "glm")
500
+ return "glm-5";
501
+ if (p === "moonshot")
502
+ return "kimi-k2.5";
503
+ return "claude-opus-4-6";
504
+ }
505
+ const model = savedModel ?? getDefault(provider);
506
+ const thinkingLevel = savedThinkingEnabled ? "medium" : undefined;
507
+ closeLogger();
508
+ await runInkTUI({
509
+ provider,
510
+ model,
511
+ cwd,
512
+ thinkingLevel,
513
+ resumeSessionPath: selectedPath,
514
+ theme: savedTheme,
515
+ });
516
+ }
517
+ async function loadTelegramConfig() {
518
+ try {
519
+ const raw = await fs.promises.readFile(getAppPaths().telegramFile, "utf-8");
520
+ const data = JSON.parse(raw);
521
+ if (data.botToken && data.userId)
522
+ return data;
523
+ return null;
524
+ }
525
+ catch {
526
+ return null;
527
+ }
528
+ }
529
+ async function saveTelegramConfig(config) {
530
+ const paths = await ensureAppDirs();
531
+ await fs.promises.writeFile(paths.telegramFile, JSON.stringify(config, null, 2), {
532
+ encoding: "utf-8",
533
+ mode: 0o600,
534
+ });
535
+ }
536
+ async function runTelegramSetup() {
537
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
538
+ const paths = await ensureAppDirs();
539
+ initLogger(paths.logFile, { version: CLI_VERSION });
540
+ log("INFO", "telegram", "Telegram setup started");
541
+ const existing = await loadTelegramConfig();
542
+ // Banner (matches Banner.tsx)
543
+ const LOGO = [
544
+ " \u2584\u2580\u2580\u2580 \u2584\u2580\u2580\u2580",
545
+ " \u2588 \u2580\u2588 \u2588 \u2580\u2588",
546
+ " \u2580\u2584\u2584\u2580 \u2580\u2584\u2584\u2580",
547
+ ];
548
+ const GRADIENT = [
549
+ "#60a5fa",
550
+ "#6da1f9",
551
+ "#7a9df7",
552
+ "#8799f5",
553
+ "#9495f3",
554
+ "#a18ff1",
555
+ "#a78bfa",
556
+ "#a18ff1",
557
+ "#9495f3",
558
+ "#8799f5",
559
+ "#7a9df7",
560
+ "#6da1f9",
561
+ ];
562
+ function gradientText(text) {
563
+ let colorIdx = 0;
564
+ return text
565
+ .split("")
566
+ .map((ch) => {
567
+ if (ch === " ")
568
+ return ch;
569
+ const color = GRADIENT[colorIdx++ % GRADIENT.length];
570
+ return chalk.hex(color)(ch);
571
+ })
572
+ .join("");
573
+ }
574
+ const GAP = " ";
575
+ console.log();
576
+ console.log(` ${gradientText(LOGO[0])}${GAP}` +
577
+ chalk.hex("#60a5fa").bold("EZ Coder") +
578
+ chalk.hex("#6b7280")(` v${CLI_VERSION}`) +
579
+ chalk.hex("#6b7280")(" · By ") +
580
+ chalk.white.bold("Nolan Grout"));
581
+ console.log(` ${gradientText(LOGO[1])}${GAP}` + chalk.hex("#a78bfa")("Telegram Setup"));
582
+ console.log(` ${gradientText(LOGO[2])}${GAP}` + chalk.hex("#6b7280")("Remote Control"));
583
+ console.log();
584
+ if (existing) {
585
+ console.log(chalk.hex("#6b7280")(" Current config:\n") +
586
+ chalk.hex("#6b7280")(` Bot token: ${existing.botToken.slice(0, 10)}...${existing.botToken.slice(-4)}\n`) +
587
+ chalk.hex("#6b7280")(` User ID: ${existing.userId}\n`));
588
+ }
589
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
590
+ try {
591
+ // Step 1: Bot token
592
+ console.log(chalk.hex("#a78bfa")(" Step 1: Bot Token\n") +
593
+ chalk.hex("#6b7280")(" 1. Open BotFather: ") +
594
+ chalk.hex("#60a5fa").underline("https://t.me/BotFather") +
595
+ "\n" +
596
+ chalk.hex("#6b7280")(" 2. Send /newbot and follow the prompts\n") +
597
+ chalk.hex("#6b7280")(" 3. Copy the bot token\n"));
598
+ const tokenPrompt = existing
599
+ ? chalk.hex("#60a5fa")(" Paste bot token (enter to keep current): ")
600
+ : chalk.hex("#60a5fa")(" Paste bot token: ");
601
+ const tokenInput = await rl.question(tokenPrompt);
602
+ const botToken = tokenInput.trim() || existing?.botToken;
603
+ if (!botToken) {
604
+ console.log(chalk.hex("#ef4444")("\n No bot token provided. Setup cancelled."));
605
+ return;
606
+ }
607
+ // Validate token format (roughly: digits:alphanumeric)
608
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
609
+ console.log(chalk.hex("#ef4444")("\n Invalid token format. Expected: 123456789:ABCdef..."));
610
+ return;
611
+ }
612
+ // Step 2: User ID
613
+ console.log(chalk.hex("#a78bfa")("\n Step 2: User ID\n") +
614
+ chalk.hex("#6b7280")(" 1. Open userinfobot: ") +
615
+ chalk.hex("#60a5fa").underline("https://t.me/userinfobot") +
616
+ "\n" +
617
+ chalk.hex("#6b7280")(" 2. Send any message — it replies with your numeric ID\n") +
618
+ chalk.hex("#6b7280")(" Only this user ID can control the bot.\n"));
619
+ const userPrompt = existing
620
+ ? chalk.hex("#60a5fa")(` Your Telegram user ID (enter to keep ${existing.userId}): `)
621
+ : chalk.hex("#60a5fa")(" Your Telegram user ID: ");
622
+ const userInput = await rl.question(userPrompt);
623
+ const userId = userInput.trim() ? parseInt(userInput.trim(), 10) : existing?.userId;
624
+ if (!userId || isNaN(userId)) {
625
+ console.log(chalk.hex("#ef4444")("\n Invalid user ID. Must be a number."));
626
+ return;
627
+ }
628
+ // Step 3: Verify bot token by calling getMe
629
+ console.log(chalk.hex("#6b7280")("\n Verifying bot token..."));
630
+ const verifyRes = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
631
+ method: "POST",
632
+ });
633
+ const verifyData = (await verifyRes.json());
634
+ if (!verifyData.ok || !verifyData.result) {
635
+ console.log(chalk.hex("#ef4444")("\n Invalid bot token — Telegram rejected it. Check and try again."));
636
+ return;
637
+ }
638
+ const botName = verifyData.result.first_name;
639
+ const botUsername = verifyData.result.username;
640
+ // Save config
641
+ await saveTelegramConfig({ botToken, userId });
642
+ log("INFO", "telegram", `Telegram setup complete: @${botUsername} (user ${userId})`);
643
+ console.log(chalk.hex("#4ade80")(`\n ✓ Connected to @${botUsername} (${botName})\n`) +
644
+ chalk.hex("#4ade80")(` ✓ Authorized user ID: ${userId}\n`) +
645
+ chalk.hex("#4ade80")(` ✓ Config saved to ${paths.telegramFile}\n\n`) +
646
+ chalk.hex("#a78bfa")(" For group chats:\n") +
647
+ chalk.hex("#6b7280")(" 1. Message @BotFather → /setprivacy → select your bot → Disable\n") +
648
+ chalk.hex("#6b7280")(" 2. Add the bot to your group\n") +
649
+ chalk.hex("#6b7280")(" 3. Send /link in the group to connect it to a project\n\n") +
650
+ chalk.hex("#60a5fa")(" To start:\n") +
651
+ chalk.hex("#6b7280")(" cd your-project && ezcoder serve\n"));
652
+ }
653
+ finally {
654
+ rl.close();
655
+ closeLogger();
656
+ }
657
+ }
658
+ // ── Serve (Telegram) ─────────────────────────────────────
659
+ async function runServe() {
660
+ const { values: serveValues } = parseArgs({
661
+ options: {
662
+ "bot-token": { type: "string" },
663
+ "user-id": { type: "string" },
664
+ provider: { type: "string" },
665
+ model: { type: "string" },
666
+ },
667
+ strict: true,
668
+ });
669
+ // Priority: CLI flags > env vars > saved config
670
+ const saved = await loadTelegramConfig();
671
+ const botToken = serveValues["bot-token"] ?? process.env.GG_TELEGRAM_BOT_TOKEN ?? saved?.botToken;
672
+ const userIdStr = serveValues["user-id"] ?? process.env.GG_TELEGRAM_USER_ID;
673
+ const userId = userIdStr ? parseInt(userIdStr, 10) : saved?.userId;
674
+ if (!botToken || !userId || isNaN(userId)) {
675
+ console.error(chalk.hex("#ef4444")("Telegram not configured.\n\n") +
676
+ "Run " +
677
+ chalk.hex("#60a5fa").bold("ezcoder telegram") +
678
+ " to set up your bot token and user ID.\n\n" +
679
+ chalk.hex("#6b7280")("Or provide manually:\n") +
680
+ chalk.hex("#6b7280")(" ezcoder serve --bot-token TOKEN --user-id ID"));
681
+ process.exit(1);
682
+ }
683
+ // Load saved settings
684
+ let savedProvider;
685
+ let savedModel;
686
+ let savedThinkingEnabled = false;
489
687
  try {
490
- return fs.readFileSync(0, "utf-8").trim();
688
+ const raw = JSON.parse(fs.readFileSync(getAppPaths().settingsFile, "utf-8"));
689
+ if (raw.defaultProvider)
690
+ savedProvider = raw.defaultProvider;
691
+ if (raw.defaultModel)
692
+ savedModel = raw.defaultModel;
693
+ if (raw.thinkingEnabled === true)
694
+ savedThinkingEnabled = true;
491
695
  }
492
696
  catch {
493
- return "";
697
+ // No settings file
698
+ }
699
+ const provider = serveValues.provider ?? savedProvider ?? "anthropic";
700
+ function getDefault(p) {
701
+ if (p === "openai")
702
+ return "gpt-5.3-codex";
703
+ if (p === "glm")
704
+ return "glm-5";
705
+ if (p === "moonshot")
706
+ return "kimi-k2.5";
707
+ return "claude-opus-4-6";
494
708
  }
709
+ const model = serveValues.model ?? savedModel ?? getDefault(provider);
710
+ const thinkingLevel = savedThinkingEnabled ? "medium" : undefined;
711
+ const paths = await ensureAppDirs();
712
+ initLogger(paths.logFile, {
713
+ version: CLI_VERSION,
714
+ provider,
715
+ model,
716
+ });
717
+ setEstimatorModel(model);
718
+ await runServeMode({
719
+ provider,
720
+ model,
721
+ cwd: process.cwd(),
722
+ version: CLI_VERSION,
723
+ thinkingLevel,
724
+ telegram: { botToken, userId },
725
+ });
495
726
  }
727
+ // ── Helpers ────────────────────────────────────────────────
496
728
  function displayName(provider) {
497
729
  if (provider === "anthropic")
498
730
  return "Anthropic";