@prestyj/cli 4.2.16 → 4.2.45

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 +458 -238
  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 -91
  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 +8 -8
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
- import { execFile, spawnSync } from "node:child_process";
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,64 +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
- // Re-exec with V8 flags for memory management during long sessions
28
- if (!process.env["EZCODER_V8_FLAGS_SET"]) {
29
- const needFlags = [];
30
- if (!process.execArgv.some((a) => a.includes("max-old-space-size")))
31
- needFlags.push("--max-old-space-size=8192");
32
- if (!process.execArgv.includes("--expose-gc"))
33
- needFlags.push("--expose-gc");
34
- if (needFlags.length > 0) {
35
- const result = spawnSync(process.execPath, [...needFlags, ...process.execArgv, ...process.argv.slice(1)], { stdio: "inherit", env: { ...process.env, EZCODER_V8_FLAGS_SET: "1" } });
36
- process.exit(result.status ?? (result.signal ? 128 : 1));
37
- }
38
- }
39
55
  const _require = createRequire(import.meta.url);
40
56
  const CLI_VERSION = _require("../package.json").version;
41
- const USAGE = `
42
- Usage: ezcoder [command] [options] [message...]
43
-
44
- Commands:
45
- login Log in to a provider via OAuth
46
- logout Log out (clear stored credentials)
47
- continue Resume the most recent session for this directory
48
-
49
- Options:
50
- -p, --provider <name> LLM provider (anthropic, openai, glm, moonshot) [default: anthropic]
51
- -m, --model <name> Model name [default: claude-opus-4-6]
52
- --base-url <url> Custom API base URL
53
- --system-prompt <text> Override system prompt
54
- --thinking <level> Thinking level (low, medium, high, max)
55
- --max-turns <n> Maximum agent loop turns [default: 40]
56
- -s, --session <path> Resume a specific session file
57
- --print Print mode: one-shot, output to stdout, then exit
58
- --json JSON mode: one-shot, output NDJSON events to stdout
59
- -v, --version Show version number
60
- -h, --help Show this help message
61
-
62
- Print mode:
63
- echo "hello" | ezcoder --print
64
- ezcoder --print "explain this code"
65
-
66
- Authentication:
67
- ezcoder login Log in (select provider interactively)
68
- ezcoder logout Log out from all providers
69
- `.trim();
70
- function getHardcodedDefault(p) {
71
- if (p === "openai")
72
- return "gpt-5.3-codex";
73
- if (p === "glm")
74
- return "glm-5";
75
- if (p === "moonshot")
76
- return "kimi-k2.5";
77
- return "claude-opus-4-6";
78
- }
79
57
  function main() {
80
58
  // Silent auto-update check (throttled, non-blocking on failure)
81
59
  const updateMessage = checkAndAutoUpdate(CLI_VERSION);
@@ -102,23 +80,48 @@ function main() {
102
80
  });
103
81
  return;
104
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
+ }
105
112
  if (subcommand === "continue") {
106
113
  // Remove "continue" so parseArgs handles remaining flags
107
114
  process.argv.splice(2, 1);
108
115
  }
109
116
  const { values, positionals } = parseArgs({
110
117
  options: {
111
- provider: { type: "string", short: "p" },
112
- model: { type: "string", short: "m" },
113
- "base-url": { type: "string" },
114
- "system-prompt": { type: "string" },
115
- thinking: { type: "string" },
116
- "max-turns": { type: "string" },
117
- session: { type: "string", short: "s" },
118
- print: { type: "boolean" },
119
- json: { type: "boolean" },
120
118
  version: { type: "boolean", short: "v" },
121
- 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" },
122
125
  },
123
126
  allowPositionals: true,
124
127
  strict: true,
@@ -127,9 +130,43 @@ function main() {
127
130
  console.log(CLI_VERSION);
128
131
  process.exit(0);
129
132
  }
130
- if (values.help) {
131
- console.log(USAGE);
132
- 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;
133
170
  }
134
171
  // Load saved settings for model/provider persistence
135
172
  let savedProvider;
@@ -150,74 +187,27 @@ function main() {
150
187
  catch {
151
188
  // No settings file or invalid JSON — use defaults
152
189
  }
153
- // Priority: CLI flag > saved setting > hardcoded default
154
- const providerFromFlag = values.provider;
155
- const provider = providerFromFlag ?? savedProvider ?? "anthropic";
156
- const defaultModel = getHardcodedDefault(provider);
157
- // Use saved model only if provider matches (or no provider flag was given)
158
- const model = values.model ?? (!providerFromFlag && savedModel ? savedModel : defaultModel);
159
- // CLI --thinking flag overrides saved setting; saved thinkingEnabled provides default
160
- const thinkingLevel = values.thinking ?? (savedThinkingEnabled ? "medium" : undefined);
161
- const maxTurns = values["max-turns"] ? parseInt(values["max-turns"], 10) : undefined;
162
- // Print mode
163
- if (values.print) {
164
- const message = positionals.join(" ").trim() || readStdinSync();
165
- if (!message) {
166
- console.error("Error: --print requires a message (positional args or stdin)");
167
- process.exit(1);
168
- }
169
- runPrintMode({
170
- message,
171
- provider,
172
- model,
173
- baseUrl: values["base-url"],
174
- systemPrompt: values["system-prompt"],
175
- cwd: process.cwd(),
176
- thinkingLevel,
177
- }).catch((err) => {
178
- log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
179
- closeLogger();
180
- process.stderr.write(formatUserError(err) + "\n");
181
- process.exit(1);
182
- });
183
- return;
184
- }
185
- // JSON mode
186
- if (values.json) {
187
- const message = positionals.join(" ").trim() || readStdinSync();
188
- if (!message) {
189
- console.error("Error: --json requires a message (positional args or stdin)");
190
- process.exit(1);
191
- }
192
- runJsonMode({
193
- message,
194
- provider,
195
- model,
196
- baseUrl: values["base-url"],
197
- systemPrompt: values["system-prompt"],
198
- cwd: process.cwd(),
199
- thinkingLevel,
200
- maxTurns,
201
- }).catch((err) => {
202
- log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
203
- closeLogger();
204
- process.stderr.write(formatUserError(err) + "\n");
205
- process.exit(1);
206
- });
207
- 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";
208
199
  }
200
+ const model = savedModel ?? getHardcodedDefault(provider);
201
+ const thinkingLevel = savedThinkingEnabled ? "medium" : undefined;
209
202
  // Interactive mode (Ink TUI)
210
203
  const cwd = process.cwd();
211
204
  const continueRecent = subcommand === "continue";
212
205
  runInkTUI({
213
206
  provider,
214
207
  model,
215
- baseUrl: values["base-url"],
216
208
  cwd,
217
209
  thinkingLevel,
218
- systemPrompt: values["system-prompt"],
219
210
  continueRecent,
220
- sessionPath: values.session,
221
211
  theme: savedTheme,
222
212
  }).catch((err) => {
223
213
  log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
@@ -228,42 +218,20 @@ function main() {
228
218
  }
229
219
  // ── Ink TUI ───────────────────────────────────────────────
230
220
  async function runInkTUI(opts) {
231
- const { cwd } = opts;
232
- let provider = opts.provider;
233
- let model = opts.model;
221
+ const { provider, model, cwd } = opts;
222
+ // Set model for token estimation accuracy
223
+ setEstimatorModel(model);
234
224
  // Resolve auth
235
225
  const paths = await ensureAppDirs();
236
- const authStorage = new AuthStorage(paths.authFile);
237
- await authStorage.load();
238
- let creds;
239
- try {
240
- creds = await authStorage.resolveCredentials(provider);
241
- }
242
- catch {
243
- // Not logged in — let user pick a provider and log in
244
- const picked = await renderLoginSelector();
245
- if (!picked) {
246
- console.log(chalk.hex("#6b7280")("Login cancelled."));
247
- process.exit(0);
248
- }
249
- provider = picked;
250
- // Update model to match the picked provider's default
251
- model = getHardcodedDefault(provider);
252
- // Check if already logged into the picked provider
253
- try {
254
- creds = await authStorage.resolveCredentials(provider);
255
- }
256
- catch {
257
- console.log(chalk.hex("#60a5fa")(`\nLogging in to ${displayName(provider)}...\n`));
258
- creds = await runInlineLogin(authStorage, provider);
259
- }
260
- }
261
226
  initLogger(paths.logFile, {
262
227
  version: CLI_VERSION,
263
228
  provider,
264
229
  model,
265
230
  thinking: opts.thinkingLevel,
266
231
  });
232
+ const authStorage = new AuthStorage(paths.authFile);
233
+ await authStorage.load();
234
+ const creds = await authStorage.resolveCredentials(provider);
267
235
  // Detect all logged-in providers and preload their credentials
268
236
  const allProviders = ["anthropic", "openai", "glm", "moonshot"];
269
237
  const loggedInProviders = [];
@@ -284,24 +252,45 @@ async function runInkTUI(opts) {
284
252
  }
285
253
  }
286
254
  }
287
- // 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
288
261
  const agents = await discoverAgents({
289
262
  globalAgentsDir: paths.agentsDir,
290
263
  projectDir: cwd,
291
264
  });
265
+ const skills = await discoverSkills({
266
+ globalSkillsDir: paths.skillsDir,
267
+ projectDir: cwd,
268
+ });
292
269
  // Build system prompt & tools (with sub-agent support)
293
- const systemPrompt = opts.systemPrompt ?? (await buildSystemPrompt(cwd));
294
- const { tools, processManager } = createTools(cwd, { agents, provider, model });
295
- // 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
296
289
  const mcpManager = new MCPClientManager();
297
290
  try {
298
291
  const providerApiKey = provider === "glm" ? credentialsByProvider["glm"]?.accessToken : undefined;
299
- const allConfigs = getMCPServers(provider, providerApiKey);
300
- const eagerConfigs = allConfigs.filter((c) => !c.lazy);
301
- const lazyConfigs = allConfigs.filter((c) => c.lazy);
302
- const mcpTools = await mcpManager.connectAll(eagerConfigs);
303
- const lazyTools = mcpManager.registerLazy(lazyConfigs);
304
- tools.push(...mcpTools, ...lazyTools);
292
+ const mcpTools = await mcpManager.connectAll(getMCPServers(provider, providerApiKey));
293
+ tools.push(...mcpTools);
305
294
  }
306
295
  catch (err) {
307
296
  log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -317,30 +306,49 @@ async function runInkTUI(opts) {
317
306
  const sessionManager = new SessionManager(paths.sessionsDir);
318
307
  let sessionPath;
319
308
  let initialHistory;
320
- if (opts.continueRecent || opts.sessionPath) {
321
- const existingPath = opts.sessionPath ?? (await sessionManager.getMostRecent(cwd));
322
- if (existingPath) {
323
- try {
324
- const loaded = await sessionManager.load(existingPath);
325
- const loadedMessages = sessionManager.getMessages(loaded.entries);
326
- if (loadedMessages.length > 0) {
327
- messages.push(...loadedMessages);
328
- sessionPath = existingPath;
329
- log("INFO", "session", `Restored session`, {
330
- path: existingPath,
331
- 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,
332
333
  });
333
- initialHistory = messagesToHistoryItems(loadedMessages);
334
- initialHistory.push({
335
- kind: "info",
336
- text: `↻ Restored session (${loadedMessages.length} messages)`,
337
- 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),
338
340
  });
339
341
  }
342
+ initialHistory = messagesToHistoryItems(loadedMessages);
343
+ initialHistory.push({
344
+ kind: "info",
345
+ text: `↻ Restored session (${loadedMessages.length} messages)`,
346
+ id: `restore-info`,
347
+ });
340
348
  }
341
- catch {
342
- // Session file corrupt or missing — start fresh
343
- }
349
+ }
350
+ catch {
351
+ // Session file corrupt or missing — start fresh
344
352
  }
345
353
  }
346
354
  // Create a new session file if we didn't reuse one
@@ -359,7 +367,6 @@ async function runInkTUI(opts) {
359
367
  maxTokens: 16384,
360
368
  thinking: opts.thinkingLevel,
361
369
  apiKey: creds.accessToken,
362
- baseUrl: opts.baseUrl,
363
370
  accountId: creds.accountId,
364
371
  cwd,
365
372
  theme: opts.theme,
@@ -372,18 +379,23 @@ async function runInkTUI(opts) {
372
379
  settingsFile: paths.settingsFile,
373
380
  mcpManager,
374
381
  authStorage,
382
+ planModeRef,
383
+ onEnterPlanRef,
384
+ onExitPlanRef,
385
+ skills,
375
386
  });
376
387
  closeLogger();
377
388
  }
378
389
  // ── Login ──────────────────────────────────────────────────
379
390
  async function runLogin() {
391
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
380
392
  const paths = await ensureAppDirs();
381
393
  initLogger(paths.logFile, { version: CLI_VERSION });
382
394
  log("INFO", "auth", "Login flow started");
383
395
  const authStorage = new AuthStorage();
384
396
  await authStorage.load();
385
397
  // Phase 1: Ink-based provider selector
386
- const provider = await renderLoginSelector();
398
+ const provider = await renderLoginSelector(CLI_VERSION);
387
399
  if (!provider) {
388
400
  console.log(chalk.hex("#6b7280")("Login cancelled."));
389
401
  return;
@@ -436,52 +448,6 @@ async function runLogin() {
436
448
  closeLogger();
437
449
  }
438
450
  }
439
- // ── Inline Login (auto-triggered when not logged in) ──────
440
- async function runInlineLogin(authStorage, provider) {
441
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
442
- try {
443
- const callbacks = {
444
- onOpenUrl: (url) => {
445
- console.log(chalk.hex("#60a5fa").bold("Opening browser..."));
446
- openBrowser(url);
447
- console.log(chalk.hex("#6b7280")("\nIf the browser didn't open, visit:\n") +
448
- chalk.hex("#6b7280")(url) +
449
- "\n");
450
- },
451
- onPromptCode: async (message) => {
452
- return rl.question(message + " ");
453
- },
454
- onStatus: (message) => {
455
- console.log(chalk.hex("#6b7280")(message));
456
- },
457
- };
458
- let creds;
459
- if (provider === "glm" || provider === "moonshot") {
460
- const keyLabel = provider === "glm" ? "Z.AI" : "Moonshot";
461
- const apiKey = await rl.question(chalk.hex("#60a5fa")(`Paste your ${keyLabel} API key: `));
462
- if (!apiKey.trim()) {
463
- console.error(chalk.hex("#ef4444")("No API key provided."));
464
- process.exit(1);
465
- }
466
- creds = {
467
- accessToken: apiKey.trim(),
468
- refreshToken: "",
469
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000 * 100,
470
- };
471
- }
472
- else {
473
- creds =
474
- provider === "anthropic" ? await loginAnthropic(callbacks) : await loginOpenAI(callbacks);
475
- }
476
- await authStorage.setCredentials(provider, creds);
477
- log("INFO", "auth", `Inline login succeeded for ${displayName(provider)}`);
478
- console.log(chalk.hex("#4ade80")(`✓ Logged in to ${displayName(provider)}!\n`));
479
- return creds;
480
- }
481
- finally {
482
- rl.close();
483
- }
484
- }
485
451
  // ── Logout ─────────────────────────────────────────────────
486
452
  async function runLogout() {
487
453
  const paths = await ensureAppDirs();
@@ -494,17 +460,271 @@ async function runLogout() {
494
460
  closeLogger();
495
461
  console.log(chalk.green("Logged out successfully."));
496
462
  }
497
- // ── Helpers ────────────────────────────────────────────────
498
- function readStdinSync() {
499
- if (process.stdin.isTTY)
500
- 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";
501
481
  try {
502
- return fs.readFileSync(0, "utf-8").trim();
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;
503
491
  }
504
492
  catch {
505
- return "";
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";
506
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
+ });
507
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
+ " \u2588\u2580\u2580\u2580 \u2580\u2580\u2580\u2588",
545
+ " \u2588\u2580\u2580 \u2584\u2580 ",
546
+ " \u2588\u2584\u2584\u2584 \u2588\u2584\u2584\u2584",
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;
687
+ try {
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;
695
+ }
696
+ catch {
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";
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
+ });
726
+ }
727
+ // ── Helpers ────────────────────────────────────────────────
508
728
  function displayName(provider) {
509
729
  if (provider === "anthropic")
510
730
  return "Anthropic";