@foxden-app/foxclaw 0.3.4 → 0.3.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/.env.example CHANGED
@@ -29,6 +29,13 @@ TELEGRAM_PREVIEW_THROTTLE_MS=800
29
29
  THREAD_LIST_LIMIT=10
30
30
  CODEX_CLI_BIN=/absolute/path/to/codex
31
31
 
32
+ # Optional: proxy for ChatGPT/Codex backend requests when FoxClaw runs as a service.
33
+ # Put these in the same env file that `foxclaw start` installs into systemd/launchd.
34
+ # HTTP_PROXY=http://127.0.0.1:7890
35
+ # HTTPS_PROXY=http://127.0.0.1:7890
36
+ # ALL_PROXY=socks5://127.0.0.1:7891
37
+ # NO_PROXY=127.0.0.1,localhost
38
+
32
39
  # Weixin (iLink): run `foxclaw weixin-login` once, then enable:
33
40
  # WX_ENABLED=true
34
41
  # Comma-separated iLink user ids allowed to chat (see account JSON linkedIlinkUserId)
package/README.md CHANGED
@@ -70,7 +70,7 @@ foxclaw doctor
70
70
  foxclaw start
71
71
  ```
72
72
 
73
- `foxclaw init` 会创建 `~/.foxclaw/.env`,并在终端里提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。任何一项都可以直接回车跳过,之后再用 `$EDITOR ~/.foxclaw/.env` 手动修改。
73
+ `foxclaw init` 会创建 `~/.foxclaw/.env`,并在终端里提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,也会询问是否写入 FoxClaw 配置,避免服务启动后 Codex 走不到同一条网络。任何一项都可以直接回车跳过,之后再用 `$EDITOR ~/.foxclaw/.env` 手动修改。
74
74
 
75
75
  跑 `doctor` 或 `start` 之前先把 `.env` 填好。私聊模式最小配置:
76
76
 
package/README_EN.md CHANGED
@@ -70,7 +70,7 @@ foxclaw doctor
70
70
  foxclaw start
71
71
  ```
72
72
 
73
- `foxclaw init` creates `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. Press Enter on any field to skip it and edit later with `$EDITOR ~/.foxclaw/.env`.
73
+ `foxclaw init` creates `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. If the current shell has proxy variables such as `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`, it also asks whether to save them into the FoxClaw config so the service uses the same network path as your working Codex CLI. Press Enter on any field to skip it and edit later with `$EDITOR ~/.foxclaw/.env`.
74
74
 
75
75
  Fill `.env` before running `doctor` or `start`. Minimum private-chat config:
76
76
 
package/dist/config.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare const DEFAULT_LOCK_PATH: string;
8
8
  export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
9
9
  export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
10
10
  export declare const DEFAULT_ENV_PATH: string;
11
+ export declare function resolveEnvPath(): string;
12
+ export declare function getLoadedEnvPath(): string | null;
11
13
  export declare function loadEnv(): void;
12
14
  export interface AppConfig {
13
15
  tgBotToken: string;
package/dist/config.js CHANGED
@@ -12,21 +12,28 @@ export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime'
12
12
  export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
13
13
  export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
14
14
  let envLoaded = false;
15
- export function loadEnv() {
16
- if (envLoaded)
17
- return;
18
- envLoaded = true;
15
+ let loadedEnvPath = null;
16
+ export function resolveEnvPath() {
19
17
  const explicitPath = process.env.FOXCLAW_ENV?.trim();
20
18
  if (explicitPath) {
21
- dotenv.config({ path: explicitPath, override: true });
22
- return;
19
+ return path.resolve(explicitPath);
23
20
  }
24
21
  const cwdEnvPath = path.join(process.cwd(), '.env');
25
22
  if (fs.existsSync(cwdEnvPath)) {
26
- dotenv.config({ path: cwdEnvPath });
27
- return;
23
+ return cwdEnvPath;
28
24
  }
29
- dotenv.config({ path: DEFAULT_ENV_PATH });
25
+ return DEFAULT_ENV_PATH;
26
+ }
27
+ export function getLoadedEnvPath() {
28
+ return loadedEnvPath;
29
+ }
30
+ export function loadEnv() {
31
+ if (envLoaded)
32
+ return;
33
+ envLoaded = true;
34
+ const envPath = resolveEnvPath();
35
+ loadedEnvPath = envPath;
36
+ dotenv.config({ path: envPath, override: Boolean(process.env.FOXCLAW_ENV?.trim()) });
30
37
  }
31
38
  export function loadConfig() {
32
39
  loadEnv();
@@ -7065,6 +7065,9 @@ function cloneAuthRetryContext(context) {
7065
7065
  };
7066
7066
  }
7067
7067
  function isCodexAuthRotationError(params) {
7068
+ if (isChatGptBackendAccessBlocked(collectCodexErrorText(params))) {
7069
+ return false;
7070
+ }
7068
7071
  const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
7069
7072
  if (code && /usageLimitExceeded|auth|unauthorized|forbidden|login/i.test(code)) {
7070
7073
  return true;
@@ -7073,15 +7076,77 @@ function isCodexAuthRotationError(params) {
7073
7076
  return /(usage limit|rate limit|not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message);
7074
7077
  }
7075
7078
  function formatCodexNotificationError(params) {
7079
+ const collected = collectCodexErrorText(params);
7080
+ const known = formatKnownCodexAccessError(collected);
7081
+ if (known) {
7082
+ return known;
7083
+ }
7076
7084
  const message = stringOrNull(params?.error?.message);
7077
7085
  if (message) {
7078
- return message;
7086
+ return clipUserFacingError(cleanUserFacingError(message));
7079
7087
  }
7080
7088
  const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
7081
7089
  if (code) {
7082
- return code;
7090
+ return clipUserFacingError(cleanUserFacingError(code));
7091
+ }
7092
+ return clipUserFacingError(cleanUserFacingError(JSON.stringify(params?.error ?? params ?? {})));
7093
+ }
7094
+ function collectCodexErrorText(params) {
7095
+ const parts = [
7096
+ stringOrNull(params?.error?.message),
7097
+ stringOrNull(params?.error?.additionalDetails),
7098
+ stringOrNull(params?.additionalDetails),
7099
+ stringOrNull(params?.error?.codexErrorInfo),
7100
+ stringOrNull(params?.error?.code),
7101
+ ].filter((part) => part !== null);
7102
+ if (parts.length > 0) {
7103
+ return parts.join(' ');
7104
+ }
7105
+ try {
7106
+ return JSON.stringify(params?.error ?? params ?? {});
7107
+ }
7108
+ catch {
7109
+ return String(params?.error ?? params ?? '');
7083
7110
  }
7084
- return JSON.stringify(params?.error ?? params ?? {});
7111
+ }
7112
+ function formatKnownCodexAccessError(raw) {
7113
+ if (!isChatGptBackendAccessBlocked(raw)) {
7114
+ return null;
7115
+ }
7116
+ const ray = raw.match(/\bcf-ray:\s*([A-Za-z0-9-]+)/i)?.[1]
7117
+ ?? raw.match(/\bRay ID:\s*([A-Za-z0-9-]+)/i)?.[1]
7118
+ ?? null;
7119
+ return `ChatGPT backend 403 Forbidden: service network/proxy/IP is blocked, not necessarily auth.json invalid. Check HTTP_PROXY/HTTPS_PROXY in the FoxClaw env file and restart FoxClaw${ray ? ` (cf-ray: ${ray})` : ''}.`;
7120
+ }
7121
+ function isChatGptBackendAccessBlocked(raw) {
7122
+ const value = raw.toLowerCase();
7123
+ const targetsChatGptBackend = value.includes('chatgpt.com/backend-api')
7124
+ || value.includes('wss://chatgpt.com/backend-api');
7125
+ const isForbidden = value.includes('403 forbidden')
7126
+ || value.includes('status 403')
7127
+ || value.includes('httpstatuscode":403')
7128
+ || value.includes('httpstatuscode:403');
7129
+ const looksLikeHtmlBlock = value.includes('<html')
7130
+ || value.includes('text/html')
7131
+ || value.includes('cf-ray')
7132
+ || value.includes('unable to load site')
7133
+ || value.includes('if you are using a vpn');
7134
+ return targetsChatGptBackend && isForbidden && looksLikeHtmlBlock;
7135
+ }
7136
+ function cleanUserFacingError(raw) {
7137
+ return raw
7138
+ .replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
7139
+ .replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
7140
+ .replace(/<[^>]+>/g, ' ')
7141
+ .replace(/\s+/g, ' ')
7142
+ .trim();
7143
+ }
7144
+ function clipUserFacingError(raw, limit = 900) {
7145
+ const cleaned = raw.trim();
7146
+ if (cleaned.length <= limit) {
7147
+ return cleaned;
7148
+ }
7149
+ return `${cleaned.slice(0, limit - 3)}...`;
7085
7150
  }
7086
7151
  function parseUserInputQuestions(params) {
7087
7152
  const rawQuestions = Array.isArray(params?.questions)
@@ -7484,7 +7549,12 @@ function formatLocalTimestamp(seconds) {
7484
7549
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
7485
7550
  }
7486
7551
  function formatShortStatusError(error) {
7487
- const message = formatUserError(error).replace(/\s+/g, ' ').trim();
7552
+ const raw = formatUserError(error);
7553
+ const known = formatKnownCodexAccessError(raw);
7554
+ if (known) {
7555
+ return known.length > 120 ? `${known.slice(0, 117)}...` : known;
7556
+ }
7557
+ const message = cleanUserFacingError(raw);
7488
7558
  return message.length > 120 ? `${message.slice(0, 117)}...` : message;
7489
7559
  }
7490
7560
  function isThreadNotFoundError(error) {
package/dist/main.js CHANGED
@@ -5,13 +5,23 @@ import process from 'node:process';
5
5
  import { createInterface } from 'node:readline/promises';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig, loadEnv, } from './config.js';
8
+ import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
9
  import { acquireProcessLock, LockHeldError } from './lock.js';
10
10
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
11
11
  const command = process.argv[2] || 'serve';
12
12
  loadEnv();
13
13
  const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
14
14
  const entryPoint = fileURLToPath(import.meta.url);
15
+ const PROXY_ENV_KEYS = [
16
+ 'HTTP_PROXY',
17
+ 'HTTPS_PROXY',
18
+ 'ALL_PROXY',
19
+ 'NO_PROXY',
20
+ 'http_proxy',
21
+ 'https_proxy',
22
+ 'all_proxy',
23
+ 'no_proxy',
24
+ ];
15
25
  async function main() {
16
26
  if (command === 'init') {
17
27
  await initConfig();
@@ -130,6 +140,9 @@ async function runServeCli() {
130
140
  updatedAt: new Date().toISOString(),
131
141
  channels: { telegram: false, weixin: false },
132
142
  });
143
+ await app.stop({ terminateServer: true }).catch((error) => {
144
+ logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
145
+ });
133
146
  store?.close();
134
147
  processLock.release();
135
148
  process.exit(0);
@@ -157,6 +170,7 @@ async function initConfig() {
157
170
  console.log(`Created ${envPath}`);
158
171
  }
159
172
  if (!canPromptForInit()) {
173
+ printProxyEnvHint(envPath);
160
174
  console.log(`Edit it manually, then run: foxclaw doctor`);
161
175
  return;
162
176
  }
@@ -171,6 +185,7 @@ async function configureEnvInteractively(envPath, existed) {
171
185
  if (existed) {
172
186
  const updateExisting = (await rl.question('Update Telegram/workspace setup fields now? [y/N]: ')).trim().toLowerCase();
173
187
  if (updateExisting !== 'y' && updateExisting !== 'yes') {
188
+ await maybeSaveProxyEnvFromShell(rl, envPath);
174
189
  console.log(`Edit it manually, then run: foxclaw doctor`);
175
190
  return;
176
191
  }
@@ -181,6 +196,7 @@ async function configureEnvInteractively(envPath, existed) {
181
196
  const updates = {};
182
197
  const skipped = [];
183
198
  const warnings = [];
199
+ Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
184
200
  const token = sanitizeEnvInput(await rl.question('Telegram bot token (TG_BOT_TOKEN): '));
185
201
  if (token) {
186
202
  updates.TG_BOT_TOKEN = token;
@@ -304,6 +320,64 @@ function writeEnvUpdates(envPath, updates) {
304
320
  }
305
321
  fs.writeFileSync(envPath, text);
306
322
  }
323
+ async function maybeSaveProxyEnvFromShell(rl, envPath) {
324
+ const proxyUpdates = detectMissingProxyEnv(envPath);
325
+ const keys = Object.keys(proxyUpdates);
326
+ if (keys.length === 0) {
327
+ return {};
328
+ }
329
+ console.log(`Detected proxy env in this shell: ${keys.join(', ')}`);
330
+ const answer = (await rl.question('Save these proxy settings to FoxClaw .env for service use? [Y/n]: ')).trim().toLowerCase();
331
+ if (answer === 'n' || answer === 'no') {
332
+ console.log(`Skipped proxy env. Add it to ${envPath} if ChatGPT access needs a proxy.`);
333
+ return {};
334
+ }
335
+ writeEnvUpdates(envPath, proxyUpdates);
336
+ console.log(`Saved ${keys.join(', ')} to ${envPath}`);
337
+ return proxyUpdates;
338
+ }
339
+ function printProxyEnvHint(envPath) {
340
+ const proxyUpdates = detectMissingProxyEnv(envPath);
341
+ const keys = Object.keys(proxyUpdates);
342
+ if (keys.length === 0) {
343
+ return;
344
+ }
345
+ console.log(`[WARN] Proxy env detected in this shell but missing from ${envPath}: ${keys.join(', ')}`);
346
+ console.log(`[WARN] Add those proxy variables to ${envPath} if ChatGPT/Codex needs a proxy.`);
347
+ }
348
+ function detectMissingProxyEnv(envPath) {
349
+ const existing = readEnvFileKeys(envPath);
350
+ const existingCanonical = new Set(Array.from(existing, canonicalProxyEnvKey));
351
+ const updates = {};
352
+ for (const key of PROXY_ENV_KEYS) {
353
+ const value = process.env[key]?.trim();
354
+ if (!value || existing.has(key) || existingCanonical.has(canonicalProxyEnvKey(key))) {
355
+ continue;
356
+ }
357
+ updates[key] = value;
358
+ }
359
+ return updates;
360
+ }
361
+ function canonicalProxyEnvKey(key) {
362
+ return key.toUpperCase();
363
+ }
364
+ function readEnvFileKeys(envPath) {
365
+ const keys = new Set();
366
+ let text = '';
367
+ try {
368
+ text = fs.readFileSync(envPath, 'utf8');
369
+ }
370
+ catch {
371
+ return keys;
372
+ }
373
+ for (const line of text.split(/\r?\n/)) {
374
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
375
+ if (match?.[1]) {
376
+ keys.add(match[1]);
377
+ }
378
+ }
379
+ return keys;
380
+ }
307
381
  function formatEnvValue(value) {
308
382
  const cleaned = value.replace(/[\r\n]/g, '').trim();
309
383
  if (!/[\s#"\\]/.test(cleaned))
@@ -373,8 +447,52 @@ function runDoctorChecks() {
373
447
  console.log(`[FAIL] default cwd missing: ${cwd}`);
374
448
  passed = false;
375
449
  }
450
+ warnIfProxyEnvMissingFromLoadedEnv();
451
+ warnIfInstalledServiceNodeLooksWrong();
376
452
  return passed;
377
453
  }
454
+ function warnIfProxyEnvMissingFromLoadedEnv() {
455
+ const envPath = serviceEnvPath();
456
+ const proxyUpdates = detectMissingProxyEnv(envPath);
457
+ const keys = Object.keys(proxyUpdates);
458
+ if (keys.length === 0) {
459
+ return;
460
+ }
461
+ console.log(`[WARN] proxy env is present in this shell but missing from ${envPath}: ${keys.join(', ')}`);
462
+ console.log(`[WARN] systemd/launchd services do not inherit your shell; add those variables to the FoxClaw env file if Codex needs a proxy.`);
463
+ }
464
+ function warnIfInstalledServiceNodeLooksWrong() {
465
+ if (process.platform !== 'linux') {
466
+ return;
467
+ }
468
+ const unitPath = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user', 'foxclaw.service');
469
+ let text = '';
470
+ try {
471
+ text = fs.readFileSync(unitPath, 'utf8');
472
+ }
473
+ catch {
474
+ return;
475
+ }
476
+ const execStart = text.match(/^ExecStart=(.+)$/m)?.[1]?.trim();
477
+ const nodePath = execStart ? systemdUnescape(execStart.split(/\s+/)[0] ?? '') : '';
478
+ if (!nodePath) {
479
+ return;
480
+ }
481
+ if (!fs.existsSync(nodePath)) {
482
+ console.log(`[WARN] installed service node is missing: ${nodePath}`);
483
+ console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
484
+ return;
485
+ }
486
+ const result = spawnSync(nodePath, ['-p', 'process.versions.node'], { encoding: 'utf8' });
487
+ const version = result.status === 0 ? result.stdout.trim() : '';
488
+ const major = Number.parseInt(version.split('.')[0] ?? '', 10);
489
+ if (Number.isFinite(major) && major >= 24) {
490
+ console.log(`[OK] service node >= 24: ${nodePath}`);
491
+ return;
492
+ }
493
+ console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
494
+ console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
495
+ }
378
496
  function installSystemd() {
379
497
  if (!hasCommand('systemctl')) {
380
498
  console.error('systemctl not found (need systemd)');
@@ -383,16 +501,14 @@ function installSystemd() {
383
501
  const unitName = 'foxclaw.service';
384
502
  const userSystemdDir = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user');
385
503
  const unitPath = path.join(userSystemdDir, unitName);
386
- const configDir = path.dirname(process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH);
504
+ const envPath = serviceEnvPath();
505
+ const configDir = path.dirname(envPath);
387
506
  const nodeBin = process.execPath;
388
507
  const nodeDir = path.dirname(nodeBin);
389
508
  const pathValue = buildServicePath(nodeDir);
390
509
  fs.mkdirSync(userSystemdDir, { recursive: true });
391
510
  fs.mkdirSync(configDir, { recursive: true });
392
511
  fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
393
- const foxclawEnvLine = process.env.FOXCLAW_ENV?.trim()
394
- ? `Environment=FOXCLAW_ENV=${systemdEscape(process.env.FOXCLAW_ENV.trim())}\n`
395
- : '';
396
512
  fs.writeFileSync(unitPath, `[Unit]
397
513
  Description=FoxClaw local Codex execution bridge
398
514
  Documentation=https://github.com/foxden-app/foxclaw
@@ -408,7 +524,8 @@ Environment=HOME=${systemdEscape(process.env.HOME || '')}
408
524
  Environment=USER=${systemdEscape(process.env.USER || '')}
409
525
  Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
410
526
  Environment=PATH=${systemdEscape(pathValue)}
411
- ${foxclawEnvLine}ExecStart=${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve
527
+ Environment=FOXCLAW_ENV=${systemdEscape(envPath)}
528
+ ExecStart=${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve
412
529
  Restart=always
413
530
  RestartSec=10
414
531
  TimeoutStopSec=45
@@ -456,15 +573,11 @@ function installLaunchd() {
456
573
  }
457
574
  const home = process.env.HOME || '';
458
575
  const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
459
- const configDir = path.dirname(process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH);
576
+ const envPath = serviceEnvPath();
577
+ const configDir = path.dirname(envPath);
460
578
  fs.mkdirSync(path.dirname(plist), { recursive: true });
461
579
  fs.mkdirSync(configDir, { recursive: true });
462
580
  fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
463
- const foxclawEnvXml = process.env.FOXCLAW_ENV?.trim()
464
- ? ` <key>FOXCLAW_ENV</key>
465
- <string>${xmlEscape(process.env.FOXCLAW_ENV.trim())}</string>
466
- `
467
- : '';
468
581
  fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
469
582
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
470
583
  <plist version="1.0">
@@ -489,7 +602,8 @@ function installLaunchd() {
489
602
  <string>${xmlEscape(process.env.USER || '')}</string>
490
603
  <key>LOGNAME</key>
491
604
  <string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
492
- ${foxclawEnvXml}
605
+ <key>FOXCLAW_ENV</key>
606
+ <string>${xmlEscape(envPath)}</string>
493
607
  </dict>
494
608
  <key>RunAtLoad</key>
495
609
  <true/>
@@ -532,6 +646,9 @@ function buildServicePath(nodeDir) {
532
646
  ];
533
647
  return parts.filter((part, index) => part && parts.indexOf(part) === index).join(':');
534
648
  }
649
+ function serviceEnvPath() {
650
+ return path.resolve(process.env.FOXCLAW_ENV?.trim() || getLoadedEnvPath() || DEFAULT_ENV_PATH);
651
+ }
535
652
  function spawnChecked(commandName, args) {
536
653
  const result = spawnSync(commandName, args, { stdio: 'inherit' });
537
654
  if (result.status !== 0) {
@@ -541,6 +658,9 @@ function spawnChecked(commandName, args) {
541
658
  function systemdEscape(value) {
542
659
  return value.replace(/\\/g, '\\\\').replace(/ /g, '\\x20');
543
660
  }
661
+ function systemdUnescape(value) {
662
+ return value.replace(/\\x20/g, ' ').replace(/\\\\/g, '\\');
663
+ }
544
664
  function xmlEscape(value) {
545
665
  return value
546
666
  .replace(/&/g, '&amp;')
@@ -182,9 +182,38 @@ Bridge logs are stored here:
182
182
  tail -f ~/.foxclaw/logs/service.log
183
183
  ```
184
184
 
185
+ ## ChatGPT Backend 403 Or Unable To Load Site
186
+
187
+ If Telegram shows `ChatGPT backend 403 Forbidden`, or the app-server log contains `Unable to load site`, `cf-ray`, or `chatgpt.com/backend-api`, the auth file is not necessarily broken. The service process is usually reaching ChatGPT with the wrong network/proxy/IP.
188
+
189
+ A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. Check the env file installed into the service:
190
+
191
+ ```bash
192
+ systemctl --user cat foxclaw.service
193
+ ```
194
+
195
+ `foxclaw init` detects proxy environment variables in the current shell and asks whether to save them into the FoxClaw `.env`. If you skipped that step, `foxclaw doctor` warns when it sees proxy variables in the shell but not in the FoxClaw env file.
196
+
197
+ Make sure the file referenced by `Environment=FOXCLAW_ENV=...` contains your proxy variables, for example:
198
+
199
+ ```dotenv
200
+ HTTP_PROXY=http://127.0.0.1:20171
201
+ HTTPS_PROXY=http://127.0.0.1:20171
202
+ ALL_PROXY=socks5://127.0.0.1:20170
203
+ NO_PROXY=127.0.0.1,localhost
204
+ ```
205
+
206
+ Restart FoxClaw after editing. The restart also restarts the managed Codex app-server so the new proxy environment takes effect:
207
+
208
+ ```bash
209
+ foxclaw restart
210
+ ```
211
+
185
212
  ## Service Starts With The Wrong Node Version
186
213
 
187
- The systemd installer captures the `node` binary from your current PATH. If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24 shell:
214
+ The systemd installer records the absolute path of the Node process that is currently running FoxClaw. It does not rely on systemd loading `nvm.sh`. If you manage multiple Node versions with nvm, run `foxclaw start` from a Node 24 shell and the service will keep using that Node 24 path.
215
+
216
+ If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24 shell:
188
217
 
189
218
  ```bash
190
219
  nvm use 24
@@ -192,7 +221,7 @@ foxclaw start
192
221
  systemctl --user status foxclaw.service
193
222
  ```
194
223
 
195
- The status output should show a Node 24 path in `ExecStart`.
224
+ The status output should show a Node 24 path in `ExecStart`. `foxclaw doctor` also checks the installed service Node path and warns if it is missing or older than 24.
196
225
 
197
226
  ## Does It Run After Reboot?
198
227
 
@@ -100,7 +100,7 @@ Both install the same published npm package. Use one global package manager cons
100
100
 
101
101
  ### 1.6 Fill In The Config
102
102
 
103
- `foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. Press Enter on any field to skip it, then edit manually if needed:
103
+ `foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. If the current shell has proxy variables such as `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`, it also asks whether to save them into the FoxClaw config so the service-side Codex app-server uses the same network. Press Enter on any field to skip it, then edit manually if needed:
104
104
 
105
105
  ```bash
106
106
  $EDITOR ~/.foxclaw/.env
@@ -183,9 +183,38 @@ Bridge 日志默认在:
183
183
  tail -f ~/.foxclaw/logs/service.log
184
184
  ```
185
185
 
186
+ ## ChatGPT 后端 403 或 Unable to load site
187
+
188
+ 如果 Telegram 里看到 `ChatGPT backend 403 Forbidden`,或者 app-server 日志里出现 `Unable to load site`、`cf-ray`、`chatgpt.com/backend-api`,通常不是 `auth.json` 文件坏了,而是服务进程访问 ChatGPT 后端时没有走正确网络。
189
+
190
+ 常见原因是:你在 shell 里配置了代理,或者项目 `.env` 里有代理,但 systemd/launchd 服务实际读的是另一个 env 文件。先看服务用的是哪个 env:
191
+
192
+ ```bash
193
+ systemctl --user cat foxclaw.service
194
+ ```
195
+
196
+ `foxclaw init` 会检测当前 shell 里的代理环境变量,并询问是否保存到 FoxClaw `.env`。如果你跳过了这一步,`foxclaw doctor` 会在发现“shell 有代理,但 FoxClaw env 没有代理”时给出 `[WARN]`。
197
+
198
+ 确认 `Environment=FOXCLAW_ENV=...` 指向的文件里有你的代理配置,例如:
199
+
200
+ ```dotenv
201
+ HTTP_PROXY=http://127.0.0.1:20171
202
+ HTTPS_PROXY=http://127.0.0.1:20171
203
+ ALL_PROXY=socks5://127.0.0.1:20170
204
+ NO_PROXY=127.0.0.1,localhost
205
+ ```
206
+
207
+ 改完后重启 FoxClaw。重启会同时重启托管的 Codex app-server,让新代理生效:
208
+
209
+ ```bash
210
+ foxclaw restart
211
+ ```
212
+
186
213
  ## 服务用了错误的 Node 版本
187
214
 
188
- systemd 安装脚本会记录当时 PATH 里的 `node`。如果你从 Node 22 或更旧版本的 shell 里安装过服务,请从 Node 24 的 shell 重新安装:
215
+ systemd 安装脚本会记录当时正在运行的 Node 绝对路径,不依赖 systemd 去加载 `nvm.sh`。如果你用 nvm 管多个 Node 版本,原则是:从 Node 24 的 shell 里执行 `foxclaw start`,服务之后就固定使用这个 Node 24 路径。
216
+
217
+ 如果你从 Node 22 或更旧版本的 shell 里安装过服务,请从 Node 24 的 shell 重新安装:
189
218
 
190
219
  ```bash
191
220
  nvm use 24
@@ -193,7 +222,7 @@ foxclaw start
193
222
  systemctl --user status foxclaw.service
194
223
  ```
195
224
 
196
- 状态输出里应该能看到 Node 24 的路径。
225
+ 状态输出里应该能看到 Node 24 的路径。`foxclaw doctor` 也会检查已安装服务里的 Node 路径,如果发现路径不存在或版本低于 24,会提示重新运行 `foxclaw start`。
197
226
 
198
227
  ## 重启后是否会自动运行
199
228
 
@@ -100,7 +100,7 @@ foxclaw init
100
100
 
101
101
  ### 1.6 填写配置
102
102
 
103
- `foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。任何一项都可以直接回车跳过,之后再手动编辑:
103
+ `foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw 配置,让服务里的 Codex app-server 使用同样的网络。任何一项都可以直接回车跳过,之后再手动编辑:
104
104
 
105
105
  ```bash
106
106
  $EDITOR ~/.foxclaw/.env
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",