@foxden-app/foxclaw 0.3.11 → 0.3.12

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,12 +29,15 @@ 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.
32
+ # Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
33
33
  # Put these in the same env file that `foxclaw start` installs into systemd/launchd.
34
+ # FoxClaw passes them to the service and enables Node's env proxy support.
34
35
  # HTTP_PROXY=http://127.0.0.1:7890
35
36
  # HTTPS_PROXY=http://127.0.0.1:7890
36
37
  # ALL_PROXY=socks5://127.0.0.1:7891
37
38
  # NO_PROXY=127.0.0.1,localhost
39
+ # Optional Linux-only fallback when a service must run through proxychains4.
40
+ # FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf
38
41
 
39
42
  # Weixin (iLink): run `foxclaw weixin-login` once, then enable:
40
43
  # WX_ENABLED=true
package/dist/main.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
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
- import { refreshFoxclawExecStartDropIns } from './systemd.js';
11
+ import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
12
12
  const rawCommand = process.argv[2];
13
13
  const command = rawCommand || 'serve';
14
14
  loadEnv();
@@ -24,6 +24,7 @@ const PROXY_ENV_KEYS = [
24
24
  'all_proxy',
25
25
  'no_proxy',
26
26
  ];
27
+ const STANDARD_NODE_PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
27
28
  async function main() {
28
29
  if (isVersionCommand(command)) {
29
30
  console.log(readPackageVersion());
@@ -493,9 +494,38 @@ function runDoctorChecks() {
493
494
  passed = false;
494
495
  }
495
496
  warnIfProxyEnvMissingFromLoadedEnv();
497
+ warnIfProxyConfigNeedsAttention();
496
498
  warnIfInstalledServiceNodeLooksWrong();
497
499
  return passed;
498
500
  }
501
+ function warnIfProxyConfigNeedsAttention() {
502
+ const proxychainsConf = process.env.FOXCLAW_PROXYCHAINS_CONF?.trim() || '';
503
+ if (proxychainsConf) {
504
+ if (process.platform !== 'linux') {
505
+ console.log('[WARN] FOXCLAW_PROXYCHAINS_CONF is only used by systemd on Linux.');
506
+ }
507
+ else if (!fs.existsSync(proxychainsConf)) {
508
+ console.log(`[WARN] FOXCLAW_PROXYCHAINS_CONF does not exist: ${proxychainsConf}`);
509
+ }
510
+ else if (!hasCommand('proxychains4')) {
511
+ console.log('[WARN] proxychains4 is not available, but FOXCLAW_PROXYCHAINS_CONF is set.');
512
+ }
513
+ else {
514
+ console.log(`[OK] proxychains config exists: ${proxychainsConf}`);
515
+ }
516
+ return;
517
+ }
518
+ const proxyKeys = PROXY_ENV_KEYS.filter((key) => proxyEnvValue(key));
519
+ if (proxyKeys.length === 0) {
520
+ return;
521
+ }
522
+ if (hasStandardNodeProxyEnv()) {
523
+ console.log(`[OK] service proxy env configured: ${proxyKeys.join(', ')}`);
524
+ return;
525
+ }
526
+ console.log('[WARN] Only ALL_PROXY/all_proxy is configured. Node service proxying works best with HTTP_PROXY/HTTPS_PROXY.');
527
+ console.log('[WARN] For SOCKS-only hosts, set FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf and run foxclaw restart.');
528
+ }
499
529
  function warnIfProxyEnvMissingFromLoadedEnv() {
500
530
  const envPath = serviceEnvPath();
501
531
  const proxyUpdates = detectMissingProxyEnv(envPath);
@@ -519,7 +549,7 @@ function warnIfInstalledServiceNodeLooksWrong() {
519
549
  return;
520
550
  }
521
551
  const execStart = text.match(/^ExecStart=(.+)$/m)?.[1]?.trim();
522
- const nodePath = execStart ? systemdUnescape(execStart.split(/\s+/)[0] ?? '') : '';
552
+ const nodePath = execStart ? extractNodePathFromExecStart(execStart) : '';
523
553
  if (!nodePath) {
524
554
  return;
525
555
  }
@@ -538,6 +568,14 @@ function warnIfInstalledServiceNodeLooksWrong() {
538
568
  console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
539
569
  console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
540
570
  }
571
+ function extractNodePathFromExecStart(execStart) {
572
+ const tokens = execStart.split(/\s+/).map(systemdUnescape).filter(Boolean);
573
+ const directNode = tokens[0] || '';
574
+ if (path.basename(directNode) === 'node') {
575
+ return directNode;
576
+ }
577
+ return tokens.find((token) => path.basename(token) === 'node') || directNode;
578
+ }
541
579
  function installSystemd() {
542
580
  if (!hasCommand('systemctl')) {
543
581
  console.error('systemctl not found (need systemd)');
@@ -551,6 +589,12 @@ function installSystemd() {
551
589
  const nodeBin = process.execPath;
552
590
  const nodeDir = path.dirname(nodeBin);
553
591
  const pathValue = buildServicePath(nodeDir);
592
+ const proxychainsConf = process.env.FOXCLAW_PROXYCHAINS_CONF?.trim() || '';
593
+ const proxychainsBin = proxychainsConf ? resolveCommand('proxychains4') || '/usr/bin/proxychains4' : '';
594
+ const nodeProxyArgs = !proxychainsConf && hasStandardNodeProxyEnv() ? ' --use-env-proxy' : '';
595
+ const execStart = proxychainsConf
596
+ ? `${systemdEscape(proxychainsBin)} -f ${systemdEscape(proxychainsConf)} ${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve`
597
+ : `${systemdEscape(nodeBin)}${nodeProxyArgs} ${systemdEscape(entryPoint)} serve`;
554
598
  fs.mkdirSync(userSystemdDir, { recursive: true });
555
599
  fs.mkdirSync(configDir, { recursive: true });
556
600
  fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
@@ -566,12 +610,13 @@ StartLimitBurst=5
566
610
  [Service]
567
611
  Type=simple
568
612
  WorkingDirectory=${systemdEscape(configDir)}
613
+ EnvironmentFile=-${systemdEscape(envPath)}
569
614
  Environment=HOME=${systemdEscape(process.env.HOME || '')}
570
615
  Environment=USER=${systemdEscape(process.env.USER || '')}
571
616
  Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
572
617
  Environment=PATH=${systemdEscape(pathValue)}
573
618
  Environment=FOXCLAW_ENV=${systemdEscape(envPath)}
574
- ExecStart=${systemdEscape(nodeBin)} ${escapedEntryPoint} serve
619
+ ExecStart=${execStart}
575
620
  Restart=always
576
621
  RestartSec=10
577
622
  TimeoutStopSec=45
@@ -580,9 +625,23 @@ KillMode=process
580
625
  [Install]
581
626
  WantedBy=default.target
582
627
  `);
583
- const dropInUpdates = refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escapedEntryPoint);
584
- for (const update of dropInUpdates) {
585
- console.log(`[OK] updated FoxClaw ExecStart override: ${update.path}`);
628
+ if (proxychainsConf) {
629
+ console.log(`[OK] systemd proxychains enabled: ${proxychainsConf}`);
630
+ }
631
+ else if (nodeProxyArgs) {
632
+ console.log('[OK] systemd Node env proxy enabled');
633
+ }
634
+ if (proxychainsConf) {
635
+ const dropInUpdates = removeFoxclawExecStartDropIns(userSystemdDir, unitName);
636
+ for (const update of dropInUpdates) {
637
+ console.log(`[OK] removed stale FoxClaw ExecStart override: ${update.path}`);
638
+ }
639
+ }
640
+ else {
641
+ const dropInUpdates = refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escapedEntryPoint);
642
+ for (const update of dropInUpdates) {
643
+ console.log(`[OK] updated FoxClaw ExecStart override: ${update.path}`);
644
+ }
586
645
  }
587
646
  spawnChecked('systemctl', ['--user', 'daemon-reload']);
588
647
  spawnChecked('systemctl', ['--user', 'enable', unitName]);
@@ -625,6 +684,9 @@ function installLaunchd() {
625
684
  const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
626
685
  const envPath = serviceEnvPath();
627
686
  const configDir = path.dirname(envPath);
687
+ const nodeProxyArgs = hasStandardNodeProxyEnv() ? ['--use-env-proxy'] : [];
688
+ const nodeProxyArgXml = nodeProxyArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
689
+ const proxyEnvXml = buildLaunchdProxyEnvironmentXml();
628
690
  fs.mkdirSync(path.dirname(plist), { recursive: true });
629
691
  fs.mkdirSync(configDir, { recursive: true });
630
692
  fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
@@ -637,7 +699,7 @@ function installLaunchd() {
637
699
  <key>ProgramArguments</key>
638
700
  <array>
639
701
  <string>${xmlEscape(process.execPath)}</string>
640
- <string>${xmlEscape(entryPoint)}</string>
702
+ ${nodeProxyArgXml ? `${nodeProxyArgXml}\n` : ''} <string>${xmlEscape(entryPoint)}</string>
641
703
  <string>serve</string>
642
704
  </array>
643
705
  <key>WorkingDirectory</key>
@@ -654,6 +716,7 @@ function installLaunchd() {
654
716
  <string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
655
717
  <key>FOXCLAW_ENV</key>
656
718
  <string>${xmlEscape(envPath)}</string>
719
+ ${proxyEnvXml}
657
720
  </dict>
658
721
  <key>RunAtLoad</key>
659
722
  <true/>
@@ -669,6 +732,9 @@ function installLaunchd() {
669
732
  spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
670
733
  spawnChecked('launchctl', ['load', plist]);
671
734
  console.log(`Installed ${plist}`);
735
+ if (nodeProxyArgs.length > 0) {
736
+ console.log('[OK] launchd Node env proxy enabled');
737
+ }
672
738
  }
673
739
  function stopLaunchd() {
674
740
  if (process.platform !== 'darwin') {
@@ -699,6 +765,23 @@ function buildServicePath(nodeDir) {
699
765
  function serviceEnvPath() {
700
766
  return path.resolve(process.env.FOXCLAW_ENV?.trim() || getLoadedEnvPath() || DEFAULT_ENV_PATH);
701
767
  }
768
+ function hasStandardNodeProxyEnv() {
769
+ return STANDARD_NODE_PROXY_ENV_KEYS.some((key) => Boolean(proxyEnvValue(key)));
770
+ }
771
+ function proxyEnvValue(key) {
772
+ return process.env[key]?.trim() || '';
773
+ }
774
+ function buildLaunchdProxyEnvironmentXml() {
775
+ const entries = [];
776
+ for (const key of PROXY_ENV_KEYS) {
777
+ const value = proxyEnvValue(key);
778
+ if (!value)
779
+ continue;
780
+ entries.push(` <key>${xmlEscape(key)}</key>`);
781
+ entries.push(` <string>${xmlEscape(value)}</string>`);
782
+ }
783
+ return entries.length > 0 ? `${entries.join('\n')}\n` : '';
784
+ }
702
785
  function spawnChecked(commandName, args) {
703
786
  const result = spawnSync(commandName, args, { stdio: 'inherit' });
704
787
  if (result.status !== 0) {
package/dist/systemd.d.ts CHANGED
@@ -3,7 +3,12 @@ export interface SystemdDropInUpdate {
3
3
  replacements: number;
4
4
  }
5
5
  export declare function refreshFoxclawExecStartDropIns(userSystemdDir: string, unitName: string, escapedEntryPoint: string): SystemdDropInUpdate[];
6
+ export declare function removeFoxclawExecStartDropIns(userSystemdDir: string, unitName: string): SystemdDropInUpdate[];
6
7
  export declare function refreshFoxclawExecStartText(text: string, escapedEntryPoint: string): {
7
8
  text: string;
8
9
  replacements: number;
9
10
  };
11
+ export declare function removeFoxclawExecStartText(text: string): {
12
+ text: string;
13
+ replacements: number;
14
+ };
package/dist/systemd.js CHANGED
@@ -29,6 +29,39 @@ export function refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escaped
29
29
  }
30
30
  return updates;
31
31
  }
32
+ export function removeFoxclawExecStartDropIns(userSystemdDir, unitName) {
33
+ const dropInDir = path.join(userSystemdDir, `${unitName}.d`);
34
+ let names;
35
+ try {
36
+ names = fs.readdirSync(dropInDir).filter((name) => name.endsWith('.conf')).sort();
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ const updates = [];
42
+ for (const name of names) {
43
+ const filePath = path.join(dropInDir, name);
44
+ let before = '';
45
+ try {
46
+ before = fs.readFileSync(filePath, 'utf8');
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ const { text, replacements } = removeFoxclawExecStartText(before);
52
+ if (replacements === 0 || text === before) {
53
+ continue;
54
+ }
55
+ if (isEmptyServiceDropIn(text)) {
56
+ fs.rmSync(filePath, { force: true });
57
+ }
58
+ else {
59
+ fs.writeFileSync(filePath, text, 'utf8');
60
+ }
61
+ updates.push({ path: filePath, replacements });
62
+ }
63
+ return updates;
64
+ }
32
65
  export function refreshFoxclawExecStartText(text, escapedEntryPoint) {
33
66
  let replacements = 0;
34
67
  const lines = text.split(/(\r?\n)/);
@@ -43,3 +76,28 @@ export function refreshFoxclawExecStartText(text, escapedEntryPoint) {
43
76
  });
44
77
  return { text: refreshed.join(''), replacements };
45
78
  }
79
+ export function removeFoxclawExecStartText(text) {
80
+ const hasFoxclawExecStart = text
81
+ .split(/\r?\n/)
82
+ .some((line) => line.startsWith('ExecStart=') && FOXCLAW_MAIN_PATH_RE.test(line));
83
+ if (!hasFoxclawExecStart) {
84
+ return { text, replacements: 0 };
85
+ }
86
+ let replacements = 0;
87
+ const lines = text.split(/(\r?\n)/);
88
+ const cleaned = lines.map((part) => {
89
+ if (!part.startsWith('ExecStart=')) {
90
+ return part;
91
+ }
92
+ replacements += 1;
93
+ return '';
94
+ });
95
+ return { text: cleaned.join(''), replacements };
96
+ }
97
+ function isEmptyServiceDropIn(text) {
98
+ const meaningfulLines = text
99
+ .split(/\r?\n/)
100
+ .map((line) => line.trim())
101
+ .filter(Boolean);
102
+ return meaningfulLines.length === 0 || (meaningfulLines.length === 1 && meaningfulLines[0] === '[Service]');
103
+ }
@@ -203,12 +203,28 @@ ALL_PROXY=socks5://127.0.0.1:20170
203
203
  NO_PROXY=127.0.0.1,localhost
204
204
  ```
205
205
 
206
+ When `HTTP_PROXY` or `HTTPS_PROXY` is configured, FoxClaw passes those variables to systemd/launchd explicitly and starts Node with `--use-env-proxy`. Do not rely on proxy variables from the current shell; service processes do not inherit them automatically.
207
+
206
208
  Restart FoxClaw after editing. The restart also restarts the managed Codex app-server so the new proxy environment takes effect:
207
209
 
208
210
  ```bash
209
211
  foxclaw restart
210
212
  ```
211
213
 
214
+ If a Linux host must use `proxychains4` to reach Telegram or ChatGPT, do not hand-write a systemd drop-in that overrides `ExecStart`. Add this to the FoxClaw env file instead:
215
+
216
+ ```dotenv
217
+ FOXCLAW_PROXYCHAINS_CONF=/home/wuya/.proxychains-rt.conf
218
+ ```
219
+
220
+ Then run:
221
+
222
+ ```bash
223
+ foxclaw restart
224
+ ```
225
+
226
+ FoxClaw writes proxychains into the main service and removes stale FoxClaw `ExecStart` overrides, so later upgrades still only need the normal `pnpm install -g` and `foxclaw restart`.
227
+
212
228
  ## Service Starts With The Wrong Node Version
213
229
 
214
230
  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` or any other shell init script. Whether you use nvm, fnm, asdf, mise, Volta, Homebrew, or system Node, run `foxclaw start` from a Node 24+ shell and the service will keep using that Node 24+ path.
@@ -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. 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:
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. When `HTTP_PROXY` or `HTTPS_PROXY` is configured, FoxClaw passes it to systemd/launchd explicitly and enables Node's env proxy support. Press Enter on any field to skip it, then edit manually if needed:
104
104
 
105
105
  ```bash
106
106
  $EDITOR ~/.foxclaw/.env
@@ -204,12 +204,28 @@ ALL_PROXY=socks5://127.0.0.1:20170
204
204
  NO_PROXY=127.0.0.1,localhost
205
205
  ```
206
206
 
207
+ 只要配置了 `HTTP_PROXY` 或 `HTTPS_PROXY`,FoxClaw 安装 systemd/launchd 时会把这些变量显式传给服务,并给 Node 加上 `--use-env-proxy`。不要依赖“当前 shell 里有代理变量”,服务进程不会自动继承它们。
208
+
207
209
  改完后重启 FoxClaw。重启会同时重启托管的 Codex app-server,让新代理生效:
208
210
 
209
211
  ```bash
210
212
  foxclaw restart
211
213
  ```
212
214
 
215
+ 如果这台 Linux 机器必须用 `proxychains4` 才能访问 Telegram 或 ChatGPT,不要手写 systemd drop-in 覆盖 `ExecStart`。在 FoxClaw env 文件里写:
216
+
217
+ ```dotenv
218
+ FOXCLAW_PROXYCHAINS_CONF=/home/wuya/.proxychains-rt.conf
219
+ ```
220
+
221
+ 然后运行:
222
+
223
+ ```bash
224
+ foxclaw restart
225
+ ```
226
+
227
+ FoxClaw 会把 proxychains 写进主 service,并清理旧的 FoxClaw `ExecStart` 覆盖,后续升级仍然只需要正常 `pnpm install -g` 和 `foxclaw restart`。
228
+
213
229
  ## 服务用了错误的 Node 版本
214
230
 
215
231
  systemd 安装脚本会记录当时正在运行的 Node 绝对路径,不依赖 systemd 去加载 `nvm.sh` 或其它 shell 初始化脚本。无论你用 nvm、fnm、asdf、mise、Volta、Homebrew 还是系统 Node,原则都是:从 Node 24+ 的 shell 里执行 `foxclaw start`,服务之后就固定使用这个 Node 24+ 路径。
@@ -100,7 +100,7 @@ foxclaw init
100
100
 
101
101
  ### 1.6 填写配置
102
102
 
103
- `foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw 配置,让服务里的 Codex app-server 使用同样的网络。任何一项都可以直接回车跳过,之后再手动编辑:
103
+ `foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw 配置。配置了 `HTTP_PROXY` `HTTPS_PROXY` 后,FoxClaw 会在安装服务时显式传给 systemd/launchd,并启用 Node 的 env proxy。任何一项都可以直接回车跳过,之后再手动编辑:
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.11",
3
+ "version": "0.3.12",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -184,6 +184,7 @@ Use this checklist when the user asks for standard closing actions, release wrap
184
184
  - If the user has a pnpm global FoxClaw install, prefer `pnpm add -g <repo-path>` so the global `foxclaw` points at the local repo.
185
185
  - Rebuild before restarting because local linked installs run `dist/main.js`.
186
186
  - Refresh systemd with the existing service env path, for example `FOXCLAW_ENV=<existing-env> <node24> dist/main.js install-systemd`. Do not run `install-systemd` from the repo without `FOXCLAW_ENV`, because it may rewrite the service to use the repo `.env`.
187
+ - Do not create systemd drop-ins that override FoxClaw `ExecStart`. For Linux hosts that require proxychains, set `FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf` in the FoxClaw env file and rerun `foxclaw restart`.
187
188
  - For macOS launchd, use the launchd install/start path from this skill and verify with `node dist/main.js status`.
188
189
  - Verify the running service reports the expected FoxClaw version in `status`.
189
190
  - If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.