@prestyj/cli 4.3.15 → 4.3.54

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 (148) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +529 -74
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +122 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +41 -23
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/auth-storage.d.ts.map +1 -1
  11. package/dist/core/auth-storage.js +14 -2
  12. package/dist/core/auth-storage.js.map +1 -1
  13. package/dist/core/auto-update.d.ts +24 -3
  14. package/dist/core/auto-update.d.ts.map +1 -1
  15. package/dist/core/auto-update.js +143 -55
  16. package/dist/core/auto-update.js.map +1 -1
  17. package/dist/core/auto-update.test.d.ts +2 -0
  18. package/dist/core/auto-update.test.d.ts.map +1 -0
  19. package/dist/core/auto-update.test.js +205 -0
  20. package/dist/core/auto-update.test.js.map +1 -0
  21. package/dist/core/compaction/compactor.d.ts +4 -0
  22. package/dist/core/compaction/compactor.d.ts.map +1 -1
  23. package/dist/core/compaction/compactor.js +17 -1
  24. package/dist/core/compaction/compactor.js.map +1 -1
  25. package/dist/core/compaction/compactor.test.js +44 -11
  26. package/dist/core/compaction/compactor.test.js.map +1 -1
  27. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  28. package/dist/core/compaction/token-estimator.js +14 -1
  29. package/dist/core/compaction/token-estimator.js.map +1 -1
  30. package/dist/core/compaction/token-estimator.test.js +1 -1
  31. package/dist/core/logger.d.ts +7 -2
  32. package/dist/core/logger.d.ts.map +1 -1
  33. package/dist/core/logger.js +48 -4
  34. package/dist/core/logger.js.map +1 -1
  35. package/dist/core/model-registry.d.ts.map +1 -1
  36. package/dist/core/model-registry.js +56 -21
  37. package/dist/core/model-registry.js.map +1 -1
  38. package/dist/core/prompt-commands.d.ts.map +1 -1
  39. package/dist/core/prompt-commands.js +356 -1
  40. package/dist/core/prompt-commands.js.map +1 -1
  41. package/dist/core/settings-manager.d.ts +1 -0
  42. package/dist/core/settings-manager.d.ts.map +1 -1
  43. package/dist/core/settings-manager.js +3 -1
  44. package/dist/core/settings-manager.js.map +1 -1
  45. package/dist/core/skills.d.ts.map +1 -1
  46. package/dist/core/skills.js +29 -14
  47. package/dist/core/skills.js.map +1 -1
  48. package/dist/modes/agent-home-mode.d.ts +21 -0
  49. package/dist/modes/agent-home-mode.d.ts.map +1 -0
  50. package/dist/modes/agent-home-mode.js +553 -0
  51. package/dist/modes/agent-home-mode.js.map +1 -0
  52. package/dist/system-prompt.d.ts +5 -1
  53. package/dist/system-prompt.d.ts.map +1 -1
  54. package/dist/system-prompt.js +118 -82
  55. package/dist/system-prompt.js.map +1 -1
  56. package/dist/tools/bash.js +1 -1
  57. package/dist/tools/bash.js.map +1 -1
  58. package/dist/tools/edit.d.ts +4 -2
  59. package/dist/tools/edit.d.ts.map +1 -1
  60. package/dist/tools/edit.js +61 -33
  61. package/dist/tools/edit.js.map +1 -1
  62. package/dist/tools/edit.test.js +80 -20
  63. package/dist/tools/edit.test.js.map +1 -1
  64. package/dist/tools/grep.d.ts.map +1 -1
  65. package/dist/tools/grep.js +12 -0
  66. package/dist/tools/grep.js.map +1 -1
  67. package/dist/tools/plan-mode.test.js +22 -6
  68. package/dist/tools/plan-mode.test.js.map +1 -1
  69. package/dist/tools/prompt-hints.d.ts +10 -0
  70. package/dist/tools/prompt-hints.d.ts.map +1 -0
  71. package/dist/tools/prompt-hints.js +45 -0
  72. package/dist/tools/prompt-hints.js.map +1 -0
  73. package/dist/tools/read.d.ts.map +1 -1
  74. package/dist/tools/read.js +32 -6
  75. package/dist/tools/read.js.map +1 -1
  76. package/dist/tools/read.test.js +23 -4
  77. package/dist/tools/read.test.js.map +1 -1
  78. package/dist/tools/subagent.d.ts.map +1 -1
  79. package/dist/tools/subagent.js +20 -3
  80. package/dist/tools/subagent.js.map +1 -1
  81. package/dist/tools/write.test.js +13 -1
  82. package/dist/tools/write.test.js.map +1 -1
  83. package/dist/ui/App.d.ts +6 -1
  84. package/dist/ui/App.d.ts.map +1 -1
  85. package/dist/ui/App.js +195 -70
  86. package/dist/ui/App.js.map +1 -1
  87. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  88. package/dist/ui/components/ActivityIndicator.js +0 -1
  89. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  90. package/dist/ui/components/EyesOverlay.d.ts +10 -0
  91. package/dist/ui/components/EyesOverlay.d.ts.map +1 -0
  92. package/dist/ui/components/EyesOverlay.js +220 -0
  93. package/dist/ui/components/EyesOverlay.js.map +1 -0
  94. package/dist/ui/components/Footer.d.ts.map +1 -1
  95. package/dist/ui/components/Footer.js +7 -6
  96. package/dist/ui/components/Footer.js.map +1 -1
  97. package/dist/ui/components/InputArea.d.ts +4 -1
  98. package/dist/ui/components/InputArea.d.ts.map +1 -1
  99. package/dist/ui/components/InputArea.js +6 -5
  100. package/dist/ui/components/InputArea.js.map +1 -1
  101. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  102. package/dist/ui/components/ModelSelector.js +2 -1
  103. package/dist/ui/components/ModelSelector.js.map +1 -1
  104. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  105. package/dist/ui/components/PlanOverlay.js +7 -1
  106. package/dist/ui/components/PlanOverlay.js.map +1 -1
  107. package/dist/ui/components/SelectList.d.ts +3 -1
  108. package/dist/ui/components/SelectList.d.ts.map +1 -1
  109. package/dist/ui/components/SelectList.js +15 -2
  110. package/dist/ui/components/SelectList.js.map +1 -1
  111. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -1
  112. package/dist/ui/components/SkillsOverlay.js +6 -59
  113. package/dist/ui/components/SkillsOverlay.js.map +1 -1
  114. package/dist/ui/components/SlashCommandMenu.d.ts.map +1 -1
  115. package/dist/ui/components/SlashCommandMenu.js +18 -8
  116. package/dist/ui/components/SlashCommandMenu.js.map +1 -1
  117. package/dist/ui/hooks/stall-retry-duplicate.test.d.ts +2 -0
  118. package/dist/ui/hooks/stall-retry-duplicate.test.d.ts.map +1 -0
  119. package/dist/ui/hooks/stall-retry-duplicate.test.js +94 -0
  120. package/dist/ui/hooks/stall-retry-duplicate.test.js.map +1 -0
  121. package/dist/ui/hooks/useAgentLoop.d.ts +1 -1
  122. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  123. package/dist/ui/hooks/useAgentLoop.js +19 -0
  124. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  125. package/dist/ui/login.d.ts.map +1 -1
  126. package/dist/ui/login.js +6 -5
  127. package/dist/ui/login.js.map +1 -1
  128. package/dist/ui/stores/create-store.d.ts +15 -0
  129. package/dist/ui/stores/create-store.d.ts.map +1 -0
  130. package/dist/ui/stores/create-store.js +30 -0
  131. package/dist/ui/stores/create-store.js.map +1 -0
  132. package/dist/ui/stores/index.d.ts +3 -0
  133. package/dist/ui/stores/index.d.ts.map +1 -0
  134. package/dist/ui/stores/index.js +3 -0
  135. package/dist/ui/stores/index.js.map +1 -0
  136. package/dist/ui/stores/taskbar-store.d.ts +19 -0
  137. package/dist/ui/stores/taskbar-store.d.ts.map +1 -0
  138. package/dist/ui/stores/taskbar-store.js +59 -0
  139. package/dist/ui/stores/taskbar-store.js.map +1 -0
  140. package/dist/ui/utils/token-to-ansi.d.ts.map +1 -1
  141. package/dist/ui/utils/token-to-ansi.js +9 -1
  142. package/dist/ui/utils/token-to-ansi.js.map +1 -1
  143. package/dist/utils/image.d.ts +18 -1
  144. package/dist/utils/image.d.ts.map +1 -1
  145. package/dist/utils/image.js +80 -31
  146. package/dist/utils/image.js.map +1 -1
  147. package/dist/utils/session-title.js +1 -1
  148. package/package.json +13 -9
package/dist/cli.js CHANGED
@@ -13,25 +13,38 @@ process.on("unhandledRejection", (reason) => {
13
13
  // Re-throw non-abort rejections so they still crash with a useful stack trace
14
14
  throw reason;
15
15
  });
16
- // Drain performance entries to prevent buffer overflow warning from dependencies
16
+ // Drain ALL performance entries to prevent unbounded memory growth.
17
+ // Node emits entries for marks, measures, resource timing (HTTP), DNS, net, etc.
18
+ // Without clearing, these accumulate across every LLM call and tool execution.
17
19
  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"] });
20
+ {
21
+ const allTypes = PerformanceObserver.supportedEntryTypes.filter((t) => t !== "gc" && t !== "function");
22
+ new PerformanceObserver((list) => {
23
+ for (const entry of list.getEntries()) {
24
+ switch (entry.entryType) {
25
+ case "measure":
26
+ performance.clearMeasures(entry.name);
27
+ break;
28
+ case "mark":
29
+ performance.clearMarks(entry.name);
30
+ break;
31
+ case "resource":
32
+ performance.clearResourceTimings();
33
+ break;
34
+ }
35
+ }
36
+ }).observe({ entryTypes: allTypes });
37
+ }
26
38
  import { parseArgs } from "node:util";
27
39
  import fs from "node:fs";
28
40
  import readline from "node:readline/promises";
29
- import { execFile } from "node:child_process";
41
+ import { execFile, spawnSync } from "node:child_process";
30
42
  import { createRequire } from "node:module";
31
43
  import { renderApp } from "./ui/render.js";
32
44
  import { runJsonMode } from "./modes/json-mode.js";
33
45
  import { runRpcMode } from "./modes/rpc-mode.js";
34
46
  import { runServeMode } from "./modes/serve-mode.js";
47
+ import { runAgentHomeMode } from "./modes/agent-home-mode.js";
35
48
  import { renderLoginSelector } from "./ui/login.js";
36
49
  import { renderSessionSelector } from "./ui/sessions.js";
37
50
  import { formatUserError } from "./utils/error-handler.js";
@@ -41,10 +54,11 @@ import { ensureAppDirs, getAppPaths, loadSavedSettings } from "./config.js";
41
54
  import { initLogger, log, closeLogger } from "./core/logger.js";
42
55
  import { setStreamDiagnostic } from "@prestyj/agent";
43
56
  import { buildSystemPrompt } from "./system-prompt.js";
57
+ import { isEyesActive, journalCount } from "@prestyj/ezcoder-eyes";
44
58
  import { createTools } from "./tools/index.js";
45
59
  import { shouldCompact, compact } from "./core/compaction/compactor.js";
46
60
  import { setEstimatorModel } from "./core/compaction/token-estimator.js";
47
- import { getContextWindow } from "./core/model-registry.js";
61
+ import { getContextWindow, getDefaultModel } from "./core/model-registry.js";
48
62
  import { MCPClientManager, getMCPServers } from "./core/mcp/index.js";
49
63
  import { discoverAgents } from "./core/agents.js";
50
64
  import { discoverSkills } from "./core/skills.js";
@@ -116,6 +130,7 @@ function printHelp() {
116
130
  const cmds = [
117
131
  ["login", "Log in to an AI provider (Anthropic, OpenAI)"],
118
132
  ["logout", "Log out and clear stored credentials"],
133
+ ["doctor", "Diagnose and fix auth/config issues"],
119
134
  ["sessions", "Browse and resume previous sessions"],
120
135
  ["continue", "Resume the most recent session"],
121
136
  ["serve", "Start the HTTP/WebSocket API server"],
@@ -131,7 +146,7 @@ function printHelp() {
131
146
  ["-h, --help", "Show this help message"],
132
147
  ["-v, --version", "Show version number"],
133
148
  ["--provider <name>", "AI provider (anthropic, xiaomi, openai, glm, moonshot)"],
134
- ["--model <name>", "Model to use (e.g. claude-sonnet-4-6, gpt-4.1)"],
149
+ ["--model <name>", "Model to use (e.g. claude-sonnet-4-6, gpt-5.5)"],
135
150
  ["--max-turns <n>", "Maximum agent turns per prompt"],
136
151
  ["--system-prompt <text>", "Override the system prompt"],
137
152
  ["--json", "JSON output mode (for sub-agents)"],
@@ -174,7 +189,7 @@ function main() {
174
189
  // Silent auto-update check (throttled, non-blocking on failure)
175
190
  const updateMessage = checkAndAutoUpdate(CLI_VERSION);
176
191
  if (updateMessage) {
177
- console.error(chalk.hex("#60a5fa")(updateMessage));
192
+ console.error(chalk.bold.hex("#4ade80")(`✨ ${updateMessage}`));
178
193
  }
179
194
  // Intercept --help / -h before anything else so it works with subcommands
180
195
  // (e.g. `ezcoder login --help` or `ezcoder --help`)
@@ -184,6 +199,24 @@ function main() {
184
199
  }
185
200
  // Handle subcommands before parseArgs
186
201
  const subcommand = process.argv[2];
202
+ // Passthrough to @prestyj/ezcoder-eyes CLI. Agents call this from bash as
203
+ // `ezcoder eyes log rough "..."` etc. — `ezcoder` is guaranteed on PATH
204
+ // (user launched it), so this avoids depending on nested bin visibility in
205
+ // global npm/pnpm installs.
206
+ if (subcommand === "eyes") {
207
+ let cliPath;
208
+ try {
209
+ cliPath = _require.resolve("@prestyj/ezcoder-eyes/cli");
210
+ }
211
+ catch {
212
+ process.stderr.write("ezcoder-eyes package not installed\n");
213
+ process.exit(1);
214
+ }
215
+ const r = spawnSync(process.execPath, [cliPath, ...process.argv.slice(3)], {
216
+ stdio: "inherit",
217
+ });
218
+ process.exit(r.status ?? 0);
219
+ }
187
220
  if (subcommand === "login") {
188
221
  runLogin().catch((err) => {
189
222
  log("ERROR", "fatal", err instanceof Error ? err.message : String(err));
@@ -231,6 +264,13 @@ function main() {
231
264
  });
232
265
  return;
233
266
  }
267
+ if (subcommand === "doctor") {
268
+ runDoctor().catch((err) => {
269
+ process.stderr.write(formatUserError(err) + "\n");
270
+ process.exit(1);
271
+ });
272
+ return;
273
+ }
234
274
  if (subcommand === "continue") {
235
275
  // Remove "continue" so parseArgs handles remaining flags
236
276
  process.argv.splice(2, 1);
@@ -261,7 +301,7 @@ function main() {
261
301
  if (values.json) {
262
302
  const message = positionals[0] ?? "";
263
303
  const jsonProvider = (values.provider ?? "anthropic");
264
- const jsonModel = values.model ?? "claude-opus-4-6";
304
+ const jsonModel = values.model ?? "claude-opus-4-7";
265
305
  const maxTurns = values["max-turns"] ? parseInt(values["max-turns"], 10) : undefined;
266
306
  const systemPrompt = values["system-prompt"];
267
307
  const cwd = process.cwd();
@@ -281,7 +321,7 @@ function main() {
281
321
  // RPC mode — headless JSON-over-stdio for IDE integrations
282
322
  if (values.rpc) {
283
323
  const rpcProvider = (values.provider ?? "anthropic");
284
- const rpcModel = values.model ?? "claude-opus-4-6";
324
+ const rpcModel = values.model ?? "claude-opus-4-7";
285
325
  const systemPrompt = values["system-prompt"];
286
326
  const cwd = process.cwd();
287
327
  runRpcMode({
@@ -301,14 +341,16 @@ function main() {
301
341
  const provider = saved.provider ?? "anthropic";
302
342
  function getHardcodedDefault(p) {
303
343
  if (p === "openai")
304
- return "gpt-5.4";
344
+ return "gpt-5.5";
305
345
  if (p === "glm")
306
346
  return "glm-5.1";
307
347
  if (p === "moonshot")
308
- return "kimi-k2.5";
348
+ return "kimi-k2.6";
309
349
  if (p === "minimax")
310
350
  return "MiniMax-M2.7";
311
- return "claude-opus-4-6";
351
+ if (p === "openrouter")
352
+ return "qwen/qwen3.6-plus";
353
+ return "claude-opus-4-7";
312
354
  }
313
355
  const model = saved.model ?? getHardcodedDefault(provider);
314
356
  const thinkingLevel = saved.thinkingEnabled ? "medium" : undefined;
@@ -331,45 +373,42 @@ function main() {
331
373
  }
332
374
  // ── Ink TUI ───────────────────────────────────────────────
333
375
  async function runInkTUI(opts) {
334
- const { provider, model, cwd } = opts;
335
- // Set model for token estimation accuracy
336
- setEstimatorModel(model);
337
- // Resolve auth
376
+ const { cwd } = opts;
377
+ // Resolve auth first so we can pick an active provider the user has
378
+ // actually logged in with — we must never default to a provider they
379
+ // haven't authenticated against.
338
380
  const paths = await ensureAppDirs();
339
- initLogger(paths.logFile, {
340
- version: CLI_VERSION,
341
- provider,
342
- model,
343
- thinking: opts.thinkingLevel,
344
- });
345
381
  // Wire stream stall diagnostics into the debug log
346
382
  setStreamDiagnostic((phase, data) => {
347
383
  log("INFO", "stream", phase, data);
348
384
  });
349
385
  const authStorage = new AuthStorage(paths.authFile);
350
386
  await authStorage.load();
351
- const creds = await authStorage.resolveCredentials(provider);
352
- // Detect all logged-in providers and preload their credentials
353
- const allProviders = ["anthropic", "xiaomi", "openai", "glm", "moonshot", "minimax"];
354
- const loggedInProviders = [];
387
+ const { provider, model, loggedInProviders } = await resolveActiveProvider(authStorage, opts.provider, opts.model);
388
+ // Preload every logged-in provider's credentials for the model switcher.
355
389
  const credentialsByProvider = {};
356
- for (const p of allProviders) {
357
- const stored = await authStorage.getCredentials(p);
358
- if (stored) {
359
- loggedInProviders.push(p);
360
- try {
361
- const resolved = await authStorage.resolveCredentials(p);
362
- credentialsByProvider[p] = {
363
- accessToken: resolved.accessToken,
364
- accountId: resolved.accountId,
365
- baseUrl: resolved.baseUrl,
366
- };
367
- }
368
- catch {
369
- // Token refresh failed — still mark as logged in
370
- }
390
+ for (const p of loggedInProviders) {
391
+ try {
392
+ const resolved = await authStorage.resolveCredentials(p);
393
+ credentialsByProvider[p] = {
394
+ accessToken: resolved.accessToken,
395
+ accountId: resolved.accountId,
396
+ baseUrl: resolved.baseUrl,
397
+ };
398
+ }
399
+ catch {
400
+ // Token refresh failed — still leave them in loggedInProviders
371
401
  }
372
402
  }
403
+ // Set model for token estimation accuracy (after provider is finalized)
404
+ setEstimatorModel(model);
405
+ initLogger(paths.logFile, {
406
+ version: CLI_VERSION,
407
+ provider,
408
+ model,
409
+ thinking: opts.thinkingLevel,
410
+ });
411
+ const creds = await authStorage.resolveCredentials(provider);
373
412
  // Ensure project-local .ezcoder directories exist
374
413
  const localEzDir = path.join(cwd, ".ezcoder");
375
414
  await fs.promises.mkdir(path.join(localEzDir, "skills"), { recursive: true });
@@ -475,6 +514,21 @@ async function runInkTUI(opts) {
475
514
  sessionPath = session.path;
476
515
  log("INFO", "session", `New session created`, { path: sessionPath });
477
516
  }
517
+ // Eyes startup banner — surface open journal signals from past sessions so the
518
+ // user isn't relying on reading agent prose to know improvements are pending.
519
+ if (isEyesActive(cwd)) {
520
+ const openCount = journalCount({ status: "open" }, cwd);
521
+ if (openCount > 0) {
522
+ const s = openCount === 1 ? "" : "s";
523
+ if (!initialHistory)
524
+ initialHistory = [];
525
+ initialHistory.push({
526
+ kind: "info",
527
+ text: `👁 Eyes: ${openCount} open improvement signal${s} from recent sessions. Run /eyes-improve to triage.`,
528
+ id: "eyes-banner",
529
+ });
530
+ }
531
+ }
478
532
  await renderApp({
479
533
  provider,
480
534
  model,
@@ -543,31 +597,27 @@ async function runLogin() {
543
597
  if (provider === "glm" ||
544
598
  provider === "moonshot" ||
545
599
  provider === "xiaomi" ||
546
- provider === "minimax") {
600
+ provider === "minimax" ||
601
+ provider === "openrouter") {
547
602
  const keyLabel = provider === "glm"
548
603
  ? "Z.AI"
549
604
  : provider === "xiaomi"
550
605
  ? "Xiaomi MiMo"
551
606
  : provider === "minimax"
552
607
  ? "MiniMax"
553
- : "Moonshot";
608
+ : provider === "openrouter"
609
+ ? "OpenRouter"
610
+ : "Moonshot";
554
611
  const apiKey = await rl.question(chalk.hex("#60a5fa")(`Paste your ${keyLabel} API key: `));
555
612
  if (!apiKey.trim()) {
556
613
  console.log(chalk.hex("#ef4444")("No API key provided. Login cancelled."));
557
614
  return;
558
615
  }
559
- let baseUrl;
560
- if (provider === "xiaomi") {
561
- const urlInput = await rl.question(chalk.hex("#60a5fa")(`Base URL ${chalk.hex("#6b7280")("(Enter for default, or paste token plan URL)")}: `));
562
- if (urlInput.trim()) {
563
- baseUrl = urlInput.trim();
564
- }
565
- }
566
616
  creds = {
567
617
  accessToken: apiKey.trim(),
568
618
  refreshToken: "",
569
619
  expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000 * 100, // ~100 years
570
- ...(baseUrl ? { baseUrl } : {}),
620
+ ...(provider === "xiaomi" ? { baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1" } : {}),
571
621
  };
572
622
  }
573
623
  else {
@@ -583,6 +633,244 @@ async function runLogin() {
583
633
  closeLogger();
584
634
  }
585
635
  }
636
+ // ── Doctor ─────────────────────────────────────────────────
637
+ async function runDoctor() {
638
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
639
+ const os = await import("node:os");
640
+ const fsP = await import("node:fs/promises");
641
+ const dim = chalk.hex("#6b7280");
642
+ const primary = chalk.hex("#60a5fa");
643
+ const accent = chalk.hex("#a78bfa");
644
+ const good = chalk.hex("#4ade80");
645
+ const warn = chalk.hex("#fbbf24");
646
+ const bad = chalk.hex("#ef4444");
647
+ // ── Banner ──────────────────────────────────────────────────
648
+ const LOGO = LOGO_LINES;
649
+ const GAP = " ";
650
+ console.log();
651
+ console.log(` ${gradientLine(LOGO[0])}${GAP}` +
652
+ primary.bold("EZ Coder") +
653
+ dim(` v${CLI_VERSION}`));
654
+ console.log(` ${gradientLine(LOGO[1])}${GAP}` + accent("Doctor"));
655
+ console.log(` ${gradientLine(LOGO[2])}${GAP}` + dim("Diagnose & Fix"));
656
+ console.log();
657
+ const home = os.homedir();
658
+ const ggDir = path.join(home, ".ezcoder");
659
+ const authFile = path.join(ggDir, "auth.json");
660
+ const lockFile = authFile + ".lock";
661
+ const myUid = process.getuid();
662
+ let fixed = 0;
663
+ // ── Environment ─────────────────────────────────────────────
664
+ console.log(accent(" Environment\n"));
665
+ console.log(dim(` Home: ${home}`));
666
+ console.log(dim(` $HOME: ${process.env.HOME ?? "(not set)"}`));
667
+ console.log(dim(` Node.js: ${process.version}`));
668
+ console.log(dim(` Platform: ${process.platform} ${process.arch}`));
669
+ console.log(dim(` UID: ${myUid} EUID: ${process.geteuid()}`));
670
+ if (process.env.HOME && process.env.HOME !== home) {
671
+ console.log(warn("\n ⚠ $HOME differs from os.homedir() — this can cause auth mismatches"));
672
+ }
673
+ if (myUid !== process.geteuid()) {
674
+ console.log(warn(" ⚠ uid ≠ euid — running with elevated privileges (sudo?)"));
675
+ console.log(dim(" Running ezcoder with sudo can cause ownership issues."));
676
+ console.log(dim(" Use without sudo, or fix after: sudo chown -R $(whoami) ~/.gg"));
677
+ }
678
+ console.log();
679
+ // ── Config Directory ────────────────────────────────────────
680
+ console.log(accent(" Config Directory\n"));
681
+ try {
682
+ const stat = await fsP.stat(ggDir);
683
+ const mode = stat.mode & 0o777;
684
+ console.log(dim(` Path: ${ggDir}`));
685
+ console.log(dim(` Mode: 0o${mode.toString(8)} UID: ${stat.uid}`));
686
+ // Fix ownership
687
+ if (stat.uid !== myUid) {
688
+ console.log(warn(` ⚠ Owned by uid ${stat.uid}, expected ${myUid}`));
689
+ try {
690
+ await fsP.chown(ggDir, myUid, process.getgid());
691
+ console.log(good(" ✓ Fixed directory ownership"));
692
+ fixed++;
693
+ }
694
+ catch {
695
+ console.log(bad(` ✗ Cannot fix — try: sudo chown -R $(whoami) ${ggDir}`));
696
+ }
697
+ }
698
+ // Fix permissions (should be 0o700)
699
+ if (mode !== 0o700) {
700
+ try {
701
+ await fsP.chmod(ggDir, 0o700);
702
+ console.log(good(" ✓ Fixed directory permissions → 0o700"));
703
+ fixed++;
704
+ }
705
+ catch {
706
+ console.log(bad(` ✗ Cannot fix — try: chmod 700 ${ggDir}`));
707
+ }
708
+ }
709
+ }
710
+ catch {
711
+ console.log(warn(` ${ggDir} missing — creating...`));
712
+ try {
713
+ await fsP.mkdir(ggDir, { recursive: true, mode: 0o700 });
714
+ console.log(good(` ✓ Created ${ggDir}`));
715
+ fixed++;
716
+ }
717
+ catch (mkErr) {
718
+ console.log(bad(` ✗ Cannot create: ${mkErr instanceof Error ? mkErr.message : String(mkErr)}`));
719
+ console.log();
720
+ return;
721
+ }
722
+ }
723
+ console.log();
724
+ // ── Lock File ───────────────────────────────────────────────
725
+ try {
726
+ const lockStat = await fsP.stat(lockFile);
727
+ const ageMs = Date.now() - lockStat.mtimeMs;
728
+ console.log(accent(" Lock File\n"));
729
+ console.log(warn(` ⚠ Stale lock found (age: ${Math.round(ageMs / 1000)}s)`));
730
+ await fsP.unlink(lockFile);
731
+ console.log(good(" ✓ Removed"));
732
+ fixed++;
733
+ console.log();
734
+ }
735
+ catch {
736
+ // No lock file — good, skip section entirely
737
+ }
738
+ // ── Auth File ───────────────────────────────────────────────
739
+ console.log(accent(" Auth File\n"));
740
+ let authData = null;
741
+ let authNeedsRewrite = false;
742
+ try {
743
+ const stat = await fsP.stat(authFile);
744
+ const mode = stat.mode & 0o777;
745
+ console.log(dim(` Path: ${authFile}`));
746
+ console.log(dim(` Size: ${stat.size} bytes Mode: 0o${mode.toString(8)} UID: ${stat.uid}`));
747
+ // Fix ownership
748
+ if (stat.uid !== myUid) {
749
+ console.log(warn(` ⚠ Owned by uid ${stat.uid}, expected ${myUid}`));
750
+ try {
751
+ await fsP.chown(authFile, myUid, process.getgid());
752
+ console.log(good(" ✓ Fixed file ownership"));
753
+ fixed++;
754
+ }
755
+ catch {
756
+ console.log(bad(` ✗ Cannot fix — try: sudo chown $(whoami) ${authFile}`));
757
+ }
758
+ }
759
+ // Fix permissions (should be 0o600)
760
+ if (mode !== 0o600) {
761
+ try {
762
+ await fsP.chmod(authFile, 0o600);
763
+ console.log(good(" ✓ Fixed file permissions → 0o600"));
764
+ fixed++;
765
+ }
766
+ catch {
767
+ console.log(bad(` ✗ Cannot fix — try: chmod 600 ${authFile}`));
768
+ }
769
+ }
770
+ // Try to read and parse
771
+ try {
772
+ const content = await fsP.readFile(authFile, "utf-8");
773
+ try {
774
+ authData = JSON.parse(content);
775
+ }
776
+ catch {
777
+ console.log(bad(" ✗ Invalid JSON — backing up and resetting"));
778
+ const backupName = `auth.json.corrupt.${Date.now()}`;
779
+ await fsP.copyFile(authFile, path.join(ggDir, backupName));
780
+ await fsP.writeFile(authFile, "{}", { encoding: "utf-8", mode: 0o600 });
781
+ console.log(good(` ✓ Corrupt file backed up as ${backupName}`));
782
+ console.log(dim(' Run "ezcoder login" to re-authenticate'));
783
+ authData = {};
784
+ fixed++;
785
+ }
786
+ }
787
+ catch (readErr) {
788
+ const code = readErr.code;
789
+ if (code === "EACCES") {
790
+ console.log(bad(" ✗ Permission denied reading auth.json"));
791
+ console.log(dim(` Try: sudo chown $(whoami) ${authFile} && chmod 600 ${authFile}`));
792
+ }
793
+ else {
794
+ console.log(bad(` ✗ Read error: ${readErr instanceof Error ? readErr.message : String(readErr)}`));
795
+ }
796
+ }
797
+ }
798
+ catch {
799
+ console.log(dim(` Path: ${authFile}`));
800
+ console.log(warn(' Not found — run "ezcoder login" to authenticate'));
801
+ }
802
+ console.log();
803
+ // ── Credentials ─────────────────────────────────────────────
804
+ if (authData && Object.keys(authData).length > 0) {
805
+ console.log(accent(" Credentials\n"));
806
+ for (const p of Object.keys(authData)) {
807
+ const cred = authData[p];
808
+ if (!cred || typeof cred !== "object") {
809
+ console.log(bad(` ✗ ${p}: invalid entry — removing`));
810
+ delete authData[p];
811
+ authNeedsRewrite = true;
812
+ fixed++;
813
+ continue;
814
+ }
815
+ if (!cred.accessToken || typeof cred.accessToken !== "string") {
816
+ console.log(bad(` ✗ ${p}: missing accessToken — removing`));
817
+ delete authData[p];
818
+ authNeedsRewrite = true;
819
+ fixed++;
820
+ continue;
821
+ }
822
+ const token = String(cred.accessToken);
823
+ const masked = token.slice(0, 8) + "..." + token.slice(-4);
824
+ const expires = typeof cred.expiresAt === "number" ? new Date(cred.expiresAt).toISOString() : "unknown";
825
+ const expired = typeof cred.expiresAt === "number" && Date.now() > cred.expiresAt;
826
+ if (expired) {
827
+ console.log(warn(` ⚠ ${p}: ${masked} expired ${expires}`));
828
+ }
829
+ else {
830
+ console.log(good(` ✓ ${p}: ${masked} expires ${expires}`));
831
+ }
832
+ }
833
+ if (authNeedsRewrite) {
834
+ try {
835
+ await fsP.writeFile(authFile, JSON.stringify(authData, null, 2), {
836
+ encoding: "utf-8",
837
+ mode: 0o600,
838
+ });
839
+ console.log(good(" ✓ Cleaned up auth.json"));
840
+ }
841
+ catch {
842
+ console.log(bad(" ✗ Failed to write cleaned auth.json"));
843
+ }
844
+ }
845
+ console.log();
846
+ }
847
+ // ── Temp Files ──────────────────────────────────────────────
848
+ try {
849
+ const entries = await fsP.readdir(ggDir);
850
+ const tmpFiles = entries.filter((e) => e.startsWith("auth.json.") && e.endsWith(".tmp"));
851
+ if (tmpFiles.length > 0) {
852
+ console.log(accent(" Temp Files\n"));
853
+ console.log(warn(` ⚠ ${tmpFiles.length} orphaned temp file(s) from interrupted writes`));
854
+ for (const tmp of tmpFiles) {
855
+ await fsP.unlink(path.join(ggDir, tmp)).catch(() => { });
856
+ }
857
+ console.log(good(` ✓ Removed ${tmpFiles.length} file(s)`));
858
+ fixed++;
859
+ console.log();
860
+ }
861
+ }
862
+ catch {
863
+ // Can't read directory — already flagged above
864
+ }
865
+ // ── Summary ─────────────────────────────────────────────────
866
+ if (fixed > 0) {
867
+ console.log(good(` ✓ Fixed ${fixed} issue${fixed > 1 ? "s" : ""}.`));
868
+ }
869
+ else {
870
+ console.log(good(" ✓ Everything looks good."));
871
+ }
872
+ console.log();
873
+ }
586
874
  // ── Logout ─────────────────────────────────────────────────
587
875
  async function runLogout() {
588
876
  const paths = await ensureAppDirs();
@@ -612,14 +900,14 @@ async function runSessions() {
612
900
  const provider = saved2.provider ?? "anthropic";
613
901
  function getDefault(p) {
614
902
  if (p === "openai")
615
- return "gpt-5.4";
903
+ return "gpt-5.5";
616
904
  if (p === "glm")
617
905
  return "glm-5.1";
618
906
  if (p === "moonshot")
619
- return "kimi-k2.5";
907
+ return "kimi-k2.6";
620
908
  if (p === "minimax")
621
909
  return "MiniMax-M2.7";
622
- return "claude-opus-4-6";
910
+ return "claude-opus-4-7";
623
911
  }
624
912
  const model = saved2.model ?? getDefault(provider);
625
913
  const thinkingLevel = saved2.thinkingEnabled ? "medium" : undefined;
@@ -800,21 +1088,12 @@ async function runServe() {
800
1088
  process.exit(1);
801
1089
  }
802
1090
  const saved3 = loadSavedSettings();
803
- const provider = serveValues.provider ?? saved3.provider ?? "anthropic";
804
- function getDefault(p) {
805
- if (p === "openai")
806
- return "gpt-5.4";
807
- if (p === "glm")
808
- return "glm-5.1";
809
- if (p === "moonshot")
810
- return "kimi-k2.5";
811
- if (p === "minimax")
812
- return "MiniMax-M2.7";
813
- return "claude-opus-4-6";
814
- }
815
- const model = serveValues.model ?? saved3.model ?? getDefault(provider);
816
1091
  const thinkingLevel = saved3.thinkingEnabled ? "medium" : undefined;
817
1092
  const paths = await ensureAppDirs();
1093
+ const authStorage = new AuthStorage(paths.authFile);
1094
+ await authStorage.load();
1095
+ const preferredProvider = serveValues.provider ?? saved3.provider ?? "anthropic";
1096
+ const { provider, model } = await resolveActiveProvider(authStorage, preferredProvider, serveValues.model ?? saved3.model);
818
1097
  initLogger(paths.logFile, {
819
1098
  version: CLI_VERSION,
820
1099
  provider,
@@ -830,7 +1109,176 @@ async function runServe() {
830
1109
  telegram: { botToken, userId },
831
1110
  });
832
1111
  }
1112
+ async function loadAgentHomeConfig() {
1113
+ try {
1114
+ const raw = await fs.promises.readFile(getAppPaths().agentHomeFile, "utf-8");
1115
+ const data = JSON.parse(raw);
1116
+ if (data.token)
1117
+ return data;
1118
+ return null;
1119
+ }
1120
+ catch {
1121
+ return null;
1122
+ }
1123
+ }
1124
+ async function saveAgentHomeConfig(config) {
1125
+ const paths = await ensureAppDirs();
1126
+ await fs.promises.writeFile(paths.agentHomeFile, JSON.stringify(config, null, 2), {
1127
+ encoding: "utf-8",
1128
+ mode: 0o600,
1129
+ });
1130
+ }
1131
+ async function runAgentHomeLogin() {
1132
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
1133
+ const paths = await ensureAppDirs();
1134
+ initLogger(paths.logFile, { version: CLI_VERSION });
1135
+ log("INFO", "agent-home", "Agent Home login started");
1136
+ const existing = await loadAgentHomeConfig();
1137
+ // Banner
1138
+ const LOGO = [
1139
+ " \u2588\u2580\u2580\u2580 \u2580\u2580\u2580\u2588",
1140
+ " \u2588\u2580\u2580 \u2584\u2580 ",
1141
+ " \u2588\u2584\u2584\u2584 \u2588\u2584\u2584\u2584",
1142
+ ];
1143
+ function gradientTextLocal(text) {
1144
+ let colorIdx = 0;
1145
+ return text
1146
+ .split("")
1147
+ .map((ch) => {
1148
+ if (ch === " ")
1149
+ return ch;
1150
+ const color = GRADIENT[colorIdx++ % GRADIENT.length];
1151
+ return chalk.hex(color)(ch);
1152
+ })
1153
+ .join("");
1154
+ }
1155
+ const GAP = " ";
1156
+ console.log();
1157
+ console.log(` ${gradientTextLocal(LOGO[0])}${GAP}` +
1158
+ chalk.hex("#60a5fa").bold("EZ Coder") +
1159
+ chalk.hex("#6b7280")(` v${CLI_VERSION}`) +
1160
+ chalk.hex("#6b7280")(" \u00b7 By ") +
1161
+ chalk.white.bold("Nolan Grout"));
1162
+ console.log(` ${gradientTextLocal(LOGO[1])}${GAP}` + chalk.hex("#a78bfa")("Agent Home Setup"));
1163
+ console.log(` ${gradientTextLocal(LOGO[2])}${GAP}` + chalk.hex("#6b7280")("Remote Control via iOS"));
1164
+ console.log();
1165
+ if (existing) {
1166
+ console.log(chalk.hex("#6b7280")(" Current config:\n") +
1167
+ chalk.hex("#6b7280")(` Token: ${existing.token.slice(0, 8)}...${existing.token.slice(-4)}\n`));
1168
+ }
1169
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1170
+ try {
1171
+ console.log(chalk.hex("#a78bfa")(" Auth Token\n") +
1172
+ chalk.hex("#6b7280")(" Open Agent Home iOS app \u2192 Settings \u2192 Generate SDK Token\n") +
1173
+ chalk.hex("#6b7280")(" Copy the token\n"));
1174
+ const tokenPrompt = existing
1175
+ ? chalk.hex("#60a5fa")(" Auth token (enter to keep current): ")
1176
+ : chalk.hex("#60a5fa")(" Auth token: ");
1177
+ const tokenInput = await rl.question(tokenPrompt);
1178
+ const token = tokenInput.trim() || existing?.token;
1179
+ if (!token) {
1180
+ console.log(chalk.hex("#ef4444")("\n No token provided. Setup cancelled."));
1181
+ return;
1182
+ }
1183
+ // Save config
1184
+ await saveAgentHomeConfig({ token });
1185
+ log("INFO", "agent-home", `Agent Home setup complete`);
1186
+ console.log(chalk.hex("#4ade80")(`\n \u2713 Token saved`) +
1187
+ "\n" +
1188
+ chalk.hex("#4ade80")(` \u2713 Config saved to ${paths.agentHomeFile}`) +
1189
+ "\n\n" +
1190
+ chalk.hex("#60a5fa")(" To start:\n") +
1191
+ chalk.hex("#6b7280")(" cd your-project && ezcoder agent-home\n"));
1192
+ }
1193
+ finally {
1194
+ rl.close();
1195
+ closeLogger();
1196
+ }
1197
+ }
1198
+ // ── Agent Home (Run) ────────────────────────────────────
1199
+ async function runAgentHome() {
1200
+ const { values: ahValues } = parseArgs({
1201
+ options: {
1202
+ token: { type: "string" },
1203
+ provider: { type: "string" },
1204
+ model: { type: "string" },
1205
+ },
1206
+ strict: true,
1207
+ });
1208
+ // Priority: CLI flags > saved config
1209
+ const saved = await loadAgentHomeConfig();
1210
+ const token = ahValues.token ?? saved?.token;
1211
+ if (!token) {
1212
+ console.error(chalk.hex("#ef4444")("Agent Home not configured.\n\n") +
1213
+ "Run " +
1214
+ chalk.hex("#60a5fa").bold("ezcoder agent-home-login") +
1215
+ " to set up your token.\n\n" +
1216
+ chalk.hex("#6b7280")("Or provide manually:\n") +
1217
+ chalk.hex("#6b7280")(" ezcoder agent-home --token TOKEN"));
1218
+ process.exit(1);
1219
+ }
1220
+ const saved4 = loadSavedSettings();
1221
+ const thinkingLevel = saved4.thinkingEnabled ? "medium" : undefined;
1222
+ const paths = await ensureAppDirs();
1223
+ const authStorage = new AuthStorage(paths.authFile);
1224
+ await authStorage.load();
1225
+ const preferredProvider = ahValues.provider ?? saved4.provider ?? "anthropic";
1226
+ const { provider, model } = await resolveActiveProvider(authStorage, preferredProvider, ahValues.model ?? saved4.model);
1227
+ initLogger(paths.logFile, {
1228
+ version: CLI_VERSION,
1229
+ provider,
1230
+ model,
1231
+ });
1232
+ setEstimatorModel(model);
1233
+ await runAgentHomeMode({
1234
+ provider,
1235
+ model,
1236
+ cwd: process.cwd(),
1237
+ version: CLI_VERSION,
1238
+ thinkingLevel,
1239
+ agentHome: { token },
1240
+ });
1241
+ }
833
1242
  // ── Helpers ────────────────────────────────────────────────
1243
+ /**
1244
+ * Resolve the provider and model to start with. If the preferred provider isn't
1245
+ * one the user is logged into, fall back to the first provider they ARE
1246
+ * logged into (in `allProviders` order). Throws if nothing is logged in.
1247
+ *
1248
+ * This prevents the CLI from crashing with "Not logged in" on startup just
1249
+ * because settings.json remembers a provider the user later logged out of.
1250
+ */
1251
+ async function resolveActiveProvider(authStorage, preferred, savedModel) {
1252
+ const allProviders = [
1253
+ "anthropic",
1254
+ "xiaomi",
1255
+ "openai",
1256
+ "glm",
1257
+ "moonshot",
1258
+ "minimax",
1259
+ "openrouter",
1260
+ ];
1261
+ const loggedInProviders = [];
1262
+ for (const p of allProviders) {
1263
+ if (await authStorage.getCredentials(p))
1264
+ loggedInProviders.push(p);
1265
+ }
1266
+ if (loggedInProviders.length === 0) {
1267
+ throw new Error('Not logged in to any provider. Run "ezcoder login" to authenticate.');
1268
+ }
1269
+ if (loggedInProviders.includes(preferred)) {
1270
+ return {
1271
+ provider: preferred,
1272
+ model: savedModel ?? getDefaultModel(preferred).id,
1273
+ loggedInProviders,
1274
+ };
1275
+ }
1276
+ // Preferred provider isn't authenticated — fall back to the first one
1277
+ // that is, and use that provider's default model (the saved model
1278
+ // belonged to a provider the user can no longer reach).
1279
+ const provider = loggedInProviders[0];
1280
+ return { provider, model: getDefaultModel(provider).id, loggedInProviders };
1281
+ }
834
1282
  function displayName(provider) {
835
1283
  if (provider === "anthropic")
836
1284
  return "Anthropic";
@@ -842,6 +1290,8 @@ function displayName(provider) {
842
1290
  return "Moonshot";
843
1291
  if (provider === "minimax")
844
1292
  return "MiniMax";
1293
+ if (provider === "openrouter")
1294
+ return "OpenRouter";
845
1295
  return "OpenAI";
846
1296
  }
847
1297
  function extractText(content) {
@@ -860,8 +1310,13 @@ function messagesToHistoryItems(msgs) {
860
1310
  for (const msg of msgs) {
861
1311
  if (msg.role === "tool") {
862
1312
  for (const tr of msg.content) {
1313
+ const text = typeof tr.content === "string"
1314
+ ? tr.content
1315
+ : tr.content
1316
+ .map((b) => (b.type === "text" ? b.text : `[image ${b.mediaType}]`))
1317
+ .join("\n");
863
1318
  toolResults.set(tr.toolCallId, {
864
- content: tr.content,
1319
+ content: text,
865
1320
  isError: tr.isError ?? false,
866
1321
  });
867
1322
  }