qwenproxy-cli 1.2.4 → 1.2.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -26,8 +26,8 @@
26
26
  "reset": "npx tsx src/reset-cooldowns.ts",
27
27
  "purge": "npx tsx src/delete-chats.ts",
28
28
  "test": "npm run test:mock && npm run test:live",
29
- "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts",
30
- "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts",
29
+ "test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts",
30
+ "test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts",
31
31
  "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
32
32
  "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
33
33
  "typecheck": "tsc --noEmit"
@@ -121,6 +121,8 @@ export function loadAccounts(): QwenAccount[] {
121
121
  export function invalidateAccountsCache(): void {
122
122
  accountsCache = null;
123
123
  accountsCacheTime = 0;
124
+ lastSyncedEnv = "";
125
+ lastSyncTime = 0;
124
126
  }
125
127
  export interface BatchAccountEntry {
126
128
  email: string;
package/src/core/paths.ts CHANGED
@@ -188,6 +188,20 @@ export function getEnvFilePath(customDataDir?: string): string {
188
188
  return path.join(customDataDir || getDataDir(), ".env");
189
189
  }
190
190
 
191
+ /**
192
+ * Path to logs directory.
193
+ */
194
+ export function getLogsDir(customDataDir?: string): string {
195
+ return path.join(customDataDir || getDataDir(), "logs");
196
+ }
197
+
198
+ /**
199
+ * Path to persistent server log file.
200
+ */
201
+ export function getServerLogFilePath(customDataDir?: string): string {
202
+ return path.join(getLogsDir(customDataDir), "server.log");
203
+ }
204
+
191
205
  /**
192
206
  * Ensures all standard directories exist on disk with proper recursive creation.
193
207
  */
@@ -195,11 +209,13 @@ export function ensureDataDirs(targetDataDir?: string): void {
195
209
  const root = targetDataDir || getDataDir();
196
210
  const dbDir = getDbDir(root);
197
211
  const profilesDir = getProfilesDir(root);
212
+ const logsDir = getLogsDir(root);
198
213
 
199
214
  try {
200
215
  if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
201
216
  if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
202
217
  if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
218
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
203
219
  } catch (err: any) {
204
220
  console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
205
221
  }
@@ -3,7 +3,9 @@
3
3
  * Captures server logs and streams them to connected TUI instances.
4
4
  */
5
5
 
6
+ import fs from "node:fs";
6
7
  import { stripAnsi } from "../tui/theme.ts";
8
+ import { getServerLogFilePath, ensureDataDirs, isRunningUnderNodeTest } from "./paths.ts";
7
9
 
8
10
  export interface ServerLogMessage {
9
11
  time: string;
@@ -11,11 +13,33 @@ export interface ServerLogMessage {
11
13
  message: string;
12
14
  }
13
15
 
14
- const MAX_LOG_HISTORY = 400;
16
+ const MAX_LOG_HISTORY = 2000;
15
17
  const logHistory: ServerLogMessage[] = [];
16
18
  const subscribers = new Set<(entry: ServerLogMessage) => void>();
17
19
  let consoleHooked = false;
18
20
 
21
+ function appendServerLogToFile(time: string, level: string, message: string): void {
22
+ if (isRunningUnderNodeTest()) return;
23
+ try {
24
+ const filePath = getServerLogFilePath();
25
+ if (fs.existsSync(filePath)) {
26
+ const stats = fs.statSync(filePath);
27
+ if (stats.size > 20 * 1024 * 1024) { // Rotate at 20MB
28
+ const oldPath = filePath + ".old";
29
+ try {
30
+ if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
31
+ fs.renameSync(filePath, oldPath);
32
+ } catch {}
33
+ }
34
+ } else {
35
+ ensureDataDirs();
36
+ }
37
+ const dateStr = new Date().toLocaleDateString("pt-BR");
38
+ const line = `[${dateStr} ${time}] [${level}] ${message}\n`;
39
+ fs.appendFileSync(filePath, line, "utf-8");
40
+ } catch {}
41
+ }
42
+
19
43
  export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string): void {
20
44
  if (!text) return;
21
45
  const clean = stripAnsi(text).trim();
@@ -55,11 +79,19 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
55
79
  .replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "")
56
80
  .replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
57
81
  if (!line) continue;
82
+
83
+ // Prevent identical consecutive duplicate logs in the same second
84
+ const last = logHistory[logHistory.length - 1];
85
+ if (last && last.time === time && last.level === level && last.message === line) {
86
+ continue;
87
+ }
88
+
58
89
  const entry: ServerLogMessage = { time, level, message: line };
59
90
  logHistory.push(entry);
60
91
  if (logHistory.length > MAX_LOG_HISTORY) {
61
92
  logHistory.shift();
62
93
  }
94
+ appendServerLogToFile(time, level, line);
63
95
 
64
96
  for (const sub of subscribers) {
65
97
  try {
@@ -6,14 +6,13 @@
6
6
  import { config } from "../core/config.ts";
7
7
  import { startServer, stopServer } from "../api/server.ts";
8
8
  import { stripAnsi } from "./theme.ts";
9
- export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
10
9
 
10
+ export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
11
11
  export interface ServerLogEntry {
12
12
  time: string;
13
13
  level: "INFO" | "WARN" | "ERROR";
14
14
  message: string;
15
15
  }
16
-
17
16
  export class ServerManager {
18
17
  private static instance: ServerManager | null = null;
19
18
 
@@ -127,19 +126,18 @@ export class ServerManager {
127
126
  }
128
127
 
129
128
  this.logEntries.push({ time, level, message: line });
130
- if (this.logEntries.length > 500) {
129
+ if (this.logEntries.length > 2000) {
131
130
  this.logEntries.shift();
132
131
  }
133
132
 
134
133
  const levelTag = level === "ERROR" ? "[ERR]" : level === "WARN" ? "[WARN]" : "";
135
134
  const formatted = `[${time}] ${levelTag ? levelTag + " " : ""}${line}`;
136
135
  this.logBuffer.push(formatted);
137
- if (this.logBuffer.length > 500) {
136
+ if (this.logBuffer.length > 2000) {
138
137
  this.logBuffer.shift();
139
138
  }
140
139
  }
141
140
  }
142
-
143
141
  public interceptLogs(): void {
144
142
  if (this.intercepted) return;
145
143
  this.intercepted = true;
@@ -3,12 +3,13 @@
3
3
  * Real-time event log viewer with level filtering (All, Warnings, Errors).
4
4
  */
5
5
 
6
+ import fs from "node:fs";
6
7
  import type { TuiView } from "../types.ts";
7
8
  import type { KeyEvent } from "../screen.ts";
8
9
  import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
9
10
  import { ServerManager } from "../server-manager.ts";
10
11
  import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
11
-
12
+ import { getServerLogFilePath, isRunningUnderNodeTest } from "../../core/paths.ts";
12
13
  export class LogsView implements TuiView {
13
14
  public readonly id = "logs";
14
15
  public readonly title = "Logs";
@@ -91,7 +92,7 @@ export class LogsView implements TuiView {
91
92
  public handleKey(key: KeyEvent): boolean | void {
92
93
  // Mouse hover or click on chips (only compute chips when mouse is on chip rows 3 to 6)
93
94
  if ((key.name === "hover" || key.name === "click") && key.mouse) {
94
- if (key.mouse.row >= 3 && key.mouse.row <= 6) {
95
+ if (key.mouse.row === 3) {
95
96
  const rawCount = ServerManager.getInstance().getLogEntries(this.filter).length;
96
97
  const { chips } = this.getChips(rawCount);
97
98
  const col = key.mouse.col;
@@ -124,7 +125,7 @@ export class LogsView implements TuiView {
124
125
  return true;
125
126
  }
126
127
  if (c.id === "copy") {
127
- this.copyLogs();
128
+ this.copyLogs(false);
128
129
  return true;
129
130
  }
130
131
  if (c.id === "clear") {
@@ -147,7 +148,7 @@ export class LogsView implements TuiView {
147
148
  return (
148
149
  col >= this.lastWidth - 3 &&
149
150
  col <= this.lastWidth &&
150
- row >= 7 &&
151
+ row >= 6 &&
151
152
  row <= 6 + this.lastVisibleCapacity
152
153
  );
153
154
  };
@@ -165,7 +166,7 @@ export class LogsView implements TuiView {
165
166
  if (key.name === "click" && key.mouse && isMouseOnScrollbar(key.mouse.col, key.mouse.row)) {
166
167
  if (this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
167
168
  this.isDraggingScrollbar = true;
168
- const r = key.mouse.row - 7;
169
+ const r = key.mouse.row - 6;
169
170
  const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
170
171
  const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
171
172
  this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
@@ -177,7 +178,7 @@ export class LogsView implements TuiView {
177
178
  // Scrollbar Drag (Hold and Move)
178
179
  if (key.name === "drag" && key.mouse) {
179
180
  if (this.isDraggingScrollbar && this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
180
- const r = key.mouse.row - 7;
181
+ const r = key.mouse.row - 6;
181
182
  const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
182
183
  const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
183
184
  this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
@@ -194,9 +195,9 @@ export class LogsView implements TuiView {
194
195
  }
195
196
  }
196
197
 
197
- // Mouse click on log rows (terminal row 7+, accounting for 2-line top margin)
198
- if (key.name === "click" && key.mouse && key.mouse.row >= 7) {
199
- const rowOffset = key.mouse.row - 7;
198
+ // Mouse click on log rows (terminal row 6+, accounting for 2-line top margin: row 4 & 5 are margin)
199
+ if (key.name === "click" && key.mouse && key.mouse.row >= 6) {
200
+ const rowOffset = key.mouse.row - 6;
200
201
  if (rowOffset >= 0 && rowOffset < this.lastVisibleCount) {
201
202
  const clickedIdx = this.lastStartIndex + rowOffset;
202
203
  if (this.selectedLogIndex === clickedIdx) {
@@ -230,16 +231,16 @@ export class LogsView implements TuiView {
230
231
  return true;
231
232
  }
232
233
 
233
- // Copy logs with 'y' or 'Y'
234
+ // Copy logs with 'y' or 'Y' (always copies full log)
234
235
  if ((key.name === "y" || key.name === "Y") && !key.ctrl) {
235
- this.copyLogs();
236
+ this.copyLogs(false);
236
237
  return true;
237
238
  }
238
239
 
239
- // Enter copies selected log if a row is selected
240
+ // Enter copies selected single line if a row is selected
240
241
  if (key.name === "return" || key.name === "enter") {
241
242
  if (this.selectedLogIndex !== null) {
242
- this.copyLogs();
243
+ this.copyLogs(true);
243
244
  return true;
244
245
  }
245
246
  }
@@ -458,22 +459,39 @@ export class LogsView implements TuiView {
458
459
  });
459
460
  return box;
460
461
  }
461
- private copyLogs(): void {
462
+
463
+ private copyLogs(singleLineOnly = false): void {
462
464
  const rawEntries = ServerManager.getInstance()
463
465
  .getLogEntries(this.filter)
464
466
  .filter((e) => e && e.message && e.message.trim().length > 0);
465
- if (rawEntries.length === 0) return;
466
467
 
467
468
  let text = "";
468
- if (this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
469
+ if (singleLineOnly && this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
469
470
  const entry = rawEntries[this.selectedLogIndex];
470
471
  text = `[${entry.time}] [${entry.level}] ${entry.message}`;
471
472
  } else {
472
- text = rawEntries
473
- .map((entry) => `[${entry.time}] [${entry.level}] ${entry.message}`)
474
- .join("\n");
473
+ // For full copy of "all" filter, prefer the complete persistent file from disk if available
474
+ if (this.filter === "all" && !isRunningUnderNodeTest()) {
475
+ try {
476
+ const logPath = getServerLogFilePath();
477
+ if (fs.existsSync(logPath)) {
478
+ const diskContent = fs.readFileSync(logPath, "utf-8");
479
+ if (diskContent && diskContent.trim().length > 0) {
480
+ text = diskContent.trim();
481
+ }
482
+ }
483
+ } catch {}
484
+ }
485
+
486
+ // Fallback to memory buffer if disk log is empty or on filtered view
487
+ if (!text && rawEntries.length > 0) {
488
+ text = rawEntries
489
+ .map((entry) => `[${entry.time}] [${entry.level}] ${entry.message}`)
490
+ .join("\n");
491
+ }
475
492
  }
476
493
 
494
+ if (!text) return;
477
495
  setClipboardText(text);
478
496
  this.copyNotification = true;
479
497
  if (this.copyTimeout) clearTimeout(this.copyTimeout);