@foxden-app/foxclaw 0.5.30 → 0.5.32
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/CHANGELOG.md +20 -0
- package/README.md +9 -1
- package/README_EN.md +8 -0
- package/dist/controller/controller.js +27 -24
- package/dist/launchd.d.ts +18 -0
- package/dist/launchd.js +75 -0
- package/dist/main.js +78 -58
- package/docs/agent-assisted-install.md +1 -0
- package/docs/install-for-beginners.md +13 -2
- package/docs/troubleshooting.md +32 -2
- package/docs/user-manual.md +8 -1
- package/docs/zh/agent-assisted-install.md +1 -0
- package/docs/zh/install-for-beginners.md +12 -1
- package/docs/zh/troubleshooting.md +32 -2
- package/docs/zh/user-manual.md +8 -1
- package/package.json +2 -1
- package/skills/foxclaw/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.5.32 - 2026-06-11
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 补齐 macOS launchd 适配检查:`doctor` 现在会检查已安装 plist 中记录的 Node 路径是否存在且为 Node 24+,并新增 `uninstall-launchd` 命令。
|
|
9
|
+
- 新增 launchd plist 生成/解析的单元测试,并补充 macOS 服务状态、日志、代理和 Node 路径排障文档。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Filled macOS launchd adaptation gaps: `doctor` now checks that the Node path recorded in the installed plist exists and is Node 24+, and `uninstall-launchd` is available.
|
|
13
|
+
- Added unit coverage for launchd plist generation/parsing and expanded macOS service status, log, proxy, and Node-path troubleshooting docs.
|
|
14
|
+
|
|
15
|
+
## 0.5.31 - 2026-06-09
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 修复自动 auth 轮转:目标候选切换后如果在 account/usage 验证阶段失败,会把该候选加入本次失败集合并继续轮询后续候选,而不是恢复原 auth 后停止。
|
|
19
|
+
- 新增回归测试覆盖候选池中间账号验证失败时继续切到下一个可用账号并重试原请求。
|
|
20
|
+
|
|
21
|
+
### English
|
|
22
|
+
- Fixed automatic auth rotation so a candidate that fails account/usage validation after switching is added to the current failure set and FoxClaw keeps polling later candidates instead of stopping after restoring the previous auth.
|
|
23
|
+
- Added regression coverage for continuing to the next usable account and retrying the original request when a middle candidate fails validation.
|
|
24
|
+
|
|
5
25
|
## 0.5.30 - 2026-06-09
|
|
6
26
|
|
|
7
27
|
### 中文
|
package/README.md
CHANGED
|
@@ -148,13 +148,20 @@ FoxClaw 的一大特色是自动多账号切换。当一个账号触发用量限
|
|
|
148
148
|
foxclaw start
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Linux 上会安装/重启用户级 systemd 服务,macOS 上安装/重载 launchd
|
|
151
|
+
Linux 上会安装/重启用户级 systemd 服务,macOS 上安装/重载 launchd。Linux 查看状态:
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
systemctl --user status foxclaw.service
|
|
155
155
|
journalctl --user -u foxclaw.service -f
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
macOS 查看状态和启动日志:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
162
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
163
|
+
```
|
|
164
|
+
|
|
158
165
|
也可以直接用包装命令:
|
|
159
166
|
|
|
160
167
|
```bash
|
|
@@ -319,6 +326,7 @@ foxclaw restart
|
|
|
319
326
|
foxclaw update
|
|
320
327
|
foxclaw stop
|
|
321
328
|
foxclaw uninstall-systemd
|
|
329
|
+
foxclaw uninstall-launchd
|
|
322
330
|
```
|
|
323
331
|
|
|
324
332
|
## 贡献
|
package/README_EN.md
CHANGED
|
@@ -155,6 +155,13 @@ systemctl --user status foxclaw.service
|
|
|
155
155
|
journalctl --user -u foxclaw.service -f
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
To inspect macOS launchd state and startup logs:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
162
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
163
|
+
```
|
|
164
|
+
|
|
158
165
|
You can also use the wrapper commands:
|
|
159
166
|
|
|
160
167
|
```bash
|
|
@@ -319,6 +326,7 @@ foxclaw restart
|
|
|
319
326
|
foxclaw update
|
|
320
327
|
foxclaw stop
|
|
321
328
|
foxclaw uninstall-systemd
|
|
329
|
+
foxclaw uninstall-launchd
|
|
322
330
|
```
|
|
323
331
|
|
|
324
332
|
## Contributing
|
|
@@ -5544,32 +5544,35 @@ export class BridgeSessionCore {
|
|
|
5544
5544
|
await this.sendMessage(rotation.scopeId, formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()));
|
|
5545
5545
|
}
|
|
5546
5546
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5547
|
+
while (true) {
|
|
5548
|
+
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5549
|
+
if (!selection) {
|
|
5550
|
+
await this.sendMessage(rotation.scopeId, t(locale, rotation.reasonKind === 'quota_limited' ? 'auth_quota_no_candidate' : 'auth_auto_no_candidate', {
|
|
5551
|
+
error: formatShortStatusError(rotation.reason),
|
|
5552
|
+
}));
|
|
5553
|
+
return false;
|
|
5554
|
+
}
|
|
5555
|
+
const { candidate, fromLabel, toLabel } = selection;
|
|
5556
|
+
const switchingKey = rotation.reasonKind === 'quota_limited'
|
|
5557
|
+
? (this.config.authAutoDeleteNeedsRepair ? 'auth_quota_switching_quiet' : 'auth_quota_switching')
|
|
5558
|
+
: (this.config.authAutoDeleteNeedsRepair ? 'auth_auto_switching_quiet' : 'auth_auto_switching');
|
|
5559
|
+
await this.sendMessage(rotation.scopeId, this.config.authAutoDeleteNeedsRepair
|
|
5560
|
+
? t(locale, switchingKey, { error: formatShortStatusError(rotation.reason) })
|
|
5561
|
+
: t(locale, switchingKey, {
|
|
5562
|
+
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
5563
|
+
error: formatShortStatusError(rotation.reason),
|
|
5564
|
+
}));
|
|
5565
|
+
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true, true, rotation.reasonKind);
|
|
5566
|
+
if (!outcome.ok) {
|
|
5567
|
+
failedTargets.add(candidate.path);
|
|
5568
|
+
continue;
|
|
5569
|
+
}
|
|
5570
|
+
if (rotation.retry) {
|
|
5571
|
+
await this.retryTurnAfterAuthRotation(rotation.scopeId, locale, rotation.retry);
|
|
5572
|
+
return true;
|
|
5573
|
+
}
|
|
5566
5574
|
return false;
|
|
5567
5575
|
}
|
|
5568
|
-
if (rotation.retry) {
|
|
5569
|
-
await this.retryTurnAfterAuthRotation(rotation.scopeId, locale, rotation.retry);
|
|
5570
|
-
return true;
|
|
5571
|
-
}
|
|
5572
|
-
return false;
|
|
5573
5576
|
}
|
|
5574
5577
|
catch (error) {
|
|
5575
5578
|
await this.handleAsyncError('codex.auth_rotation', error, rotation.scopeId);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface FoxclawLaunchdPlistOptions {
|
|
2
|
+
label: string;
|
|
3
|
+
nodePath: string;
|
|
4
|
+
nodeArgs: string[];
|
|
5
|
+
entryPoint: string;
|
|
6
|
+
workingDirectory: string;
|
|
7
|
+
pathValue: string;
|
|
8
|
+
home: string;
|
|
9
|
+
user: string;
|
|
10
|
+
logname: string;
|
|
11
|
+
envPath: string;
|
|
12
|
+
proxyEnv: Record<string, string>;
|
|
13
|
+
stdoutPath: string;
|
|
14
|
+
stderrPath: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildFoxclawLaunchdPlistText(options: FoxclawLaunchdPlistOptions): string;
|
|
17
|
+
export declare function extractNodePathFromLaunchdPlist(plistText: string): string;
|
|
18
|
+
export declare function xmlEscape(value: string): string;
|
package/dist/launchd.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function buildFoxclawLaunchdPlistText(options) {
|
|
2
|
+
const nodeArgXml = options.nodeArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
|
|
3
|
+
const proxyEnvXml = buildEnvironmentVariablesXml(options.proxyEnv, 4);
|
|
4
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
6
|
+
<plist version="1.0">
|
|
7
|
+
<dict>
|
|
8
|
+
<key>Label</key>
|
|
9
|
+
<string>${xmlEscape(options.label)}</string>
|
|
10
|
+
<key>ProgramArguments</key>
|
|
11
|
+
<array>
|
|
12
|
+
<string>${xmlEscape(options.nodePath)}</string>
|
|
13
|
+
${nodeArgXml ? `${nodeArgXml}\n` : ''} <string>${xmlEscape(options.entryPoint)}</string>
|
|
14
|
+
<string>serve</string>
|
|
15
|
+
</array>
|
|
16
|
+
<key>WorkingDirectory</key>
|
|
17
|
+
<string>${xmlEscape(options.workingDirectory)}</string>
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>PATH</key>
|
|
21
|
+
<string>${xmlEscape(options.pathValue)}</string>
|
|
22
|
+
<key>HOME</key>
|
|
23
|
+
<string>${xmlEscape(options.home)}</string>
|
|
24
|
+
<key>USER</key>
|
|
25
|
+
<string>${xmlEscape(options.user)}</string>
|
|
26
|
+
<key>LOGNAME</key>
|
|
27
|
+
<string>${xmlEscape(options.logname)}</string>
|
|
28
|
+
<key>FOXCLAW_ENV</key>
|
|
29
|
+
<string>${xmlEscape(options.envPath)}</string>
|
|
30
|
+
${proxyEnvXml}
|
|
31
|
+
</dict>
|
|
32
|
+
<key>RunAtLoad</key>
|
|
33
|
+
<true/>
|
|
34
|
+
<key>KeepAlive</key>
|
|
35
|
+
<true/>
|
|
36
|
+
<key>StandardOutPath</key>
|
|
37
|
+
<string>${xmlEscape(options.stdoutPath)}</string>
|
|
38
|
+
<key>StandardErrorPath</key>
|
|
39
|
+
<string>${xmlEscape(options.stderrPath)}</string>
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export function extractNodePathFromLaunchdPlist(plistText) {
|
|
45
|
+
const programArguments = plistText.match(/<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
46
|
+
const firstArgument = programArguments?.[1]?.match(/<string>([\s\S]*?)<\/string>/)?.[1] ?? '';
|
|
47
|
+
return firstArgument ? xmlUnescape(firstArgument.trim()) : '';
|
|
48
|
+
}
|
|
49
|
+
export function xmlEscape(value) {
|
|
50
|
+
return value
|
|
51
|
+
.replace(/&/g, '&')
|
|
52
|
+
.replace(/</g, '<')
|
|
53
|
+
.replace(/>/g, '>')
|
|
54
|
+
.replace(/"/g, '"')
|
|
55
|
+
.replace(/'/g, ''');
|
|
56
|
+
}
|
|
57
|
+
function xmlUnescape(value) {
|
|
58
|
+
return value
|
|
59
|
+
.replace(/'/g, "'")
|
|
60
|
+
.replace(/"/g, '"')
|
|
61
|
+
.replace(/>/g, '>')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/&/g, '&');
|
|
64
|
+
}
|
|
65
|
+
function buildEnvironmentVariablesXml(values, indent) {
|
|
66
|
+
const prefix = ' '.repeat(indent);
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const [key, value] of Object.entries(values)) {
|
|
69
|
+
if (!value)
|
|
70
|
+
continue;
|
|
71
|
+
entries.push(`${prefix}<key>${xmlEscape(key)}</key>`);
|
|
72
|
+
entries.push(`${prefix}<string>${xmlEscape(value)}</string>`);
|
|
73
|
+
}
|
|
74
|
+
return entries.length > 0 ? `${entries.join('\n')}\n` : '';
|
|
75
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -11,6 +11,7 @@ import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PA
|
|
|
11
11
|
import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
|
|
12
12
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
13
13
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
14
|
+
import { buildFoxclawLaunchdPlistText, extractNodePathFromLaunchdPlist, } from './launchd.js';
|
|
14
15
|
import { buildFoxclawSystemdUnitText, buildSystemdRestartHelperArgs, cgroupContainsSystemdUnit, refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns, } from './systemd.js';
|
|
15
16
|
import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
|
|
16
17
|
const rawCommand = process.argv[2];
|
|
@@ -113,6 +114,10 @@ async function main() {
|
|
|
113
114
|
uninstallSystemd();
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
117
|
+
if (command === 'uninstall-launchd') {
|
|
118
|
+
uninstallLaunchd();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
116
121
|
if (command === 'install-launchd') {
|
|
117
122
|
requireNode24(command);
|
|
118
123
|
installLaunchd();
|
|
@@ -173,7 +178,7 @@ Usage:
|
|
|
173
178
|
foxclaw start|restart|stop
|
|
174
179
|
foxclaw update
|
|
175
180
|
foxclaw install-systemd|uninstall-systemd
|
|
176
|
-
foxclaw install-launchd
|
|
181
|
+
foxclaw install-launchd|uninstall-launchd
|
|
177
182
|
foxclaw weixin-login [account-id]
|
|
178
183
|
foxclaw --version
|
|
179
184
|
foxclaw --help`);
|
|
@@ -1344,6 +1349,7 @@ function runDoctorChecks() {
|
|
|
1344
1349
|
warnIfProxyEnvMissingFromLoadedEnv();
|
|
1345
1350
|
warnIfProxyConfigNeedsAttention();
|
|
1346
1351
|
warnIfInstalledServiceNodeLooksWrong();
|
|
1352
|
+
warnIfInstalledLaunchdNodeLooksWrong();
|
|
1347
1353
|
warnIfSystemdUserLingerDisabled();
|
|
1348
1354
|
return passed;
|
|
1349
1355
|
}
|
|
@@ -1373,7 +1379,12 @@ function warnIfProxyConfigNeedsAttention() {
|
|
|
1373
1379
|
return;
|
|
1374
1380
|
}
|
|
1375
1381
|
console.log('[WARN] Only ALL_PROXY/all_proxy is configured. Node service proxying works best with HTTP_PROXY/HTTPS_PROXY.');
|
|
1376
|
-
|
|
1382
|
+
if (process.platform === 'linux') {
|
|
1383
|
+
console.log('[WARN] For SOCKS-only hosts, set FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf and run foxclaw restart.');
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
console.log('[WARN] On macOS launchd, prefer HTTP_PROXY/HTTPS_PROXY; FOXCLAW_PROXYCHAINS_CONF is Linux-only.');
|
|
1387
|
+
}
|
|
1377
1388
|
}
|
|
1378
1389
|
function warnIfProxyEnvMissingFromLoadedEnv() {
|
|
1379
1390
|
const envPath = serviceEnvPath();
|
|
@@ -1417,6 +1428,37 @@ function warnIfInstalledServiceNodeLooksWrong() {
|
|
|
1417
1428
|
console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
|
|
1418
1429
|
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
|
|
1419
1430
|
}
|
|
1431
|
+
function warnIfInstalledLaunchdNodeLooksWrong() {
|
|
1432
|
+
if (process.platform !== 'darwin') {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const plistPath = launchdPlistPath();
|
|
1436
|
+
let text = '';
|
|
1437
|
+
try {
|
|
1438
|
+
text = fs.readFileSync(plistPath, 'utf8');
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
const nodePath = extractNodePathFromLaunchdPlist(text);
|
|
1444
|
+
if (!nodePath) {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (!fs.existsSync(nodePath)) {
|
|
1448
|
+
console.log(`[WARN] installed launchd node is missing: ${nodePath}`);
|
|
1449
|
+
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the launchd plist.');
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const result = spawnSync(nodePath, ['-p', 'process.versions.node'], { encoding: 'utf8' });
|
|
1453
|
+
const version = result.status === 0 ? result.stdout.trim() : '';
|
|
1454
|
+
const major = Number.parseInt(version.split('.')[0] ?? '', 10);
|
|
1455
|
+
if (Number.isFinite(major) && major >= 24) {
|
|
1456
|
+
console.log(`[OK] launchd node >= 24: ${nodePath}`);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
console.log(`[WARN] installed launchd node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
|
|
1460
|
+
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the launchd plist.');
|
|
1461
|
+
}
|
|
1420
1462
|
function warnIfSystemdUserLingerDisabled() {
|
|
1421
1463
|
if (process.platform !== 'linux' || !hasCommand('loginctl')) {
|
|
1422
1464
|
return;
|
|
@@ -1588,54 +1630,28 @@ function installLaunchd() {
|
|
|
1588
1630
|
process.exit(1);
|
|
1589
1631
|
}
|
|
1590
1632
|
const home = process.env.HOME || '';
|
|
1591
|
-
const plist =
|
|
1633
|
+
const plist = launchdPlistPath();
|
|
1592
1634
|
const envPath = serviceEnvPath();
|
|
1593
1635
|
const configDir = path.dirname(envPath);
|
|
1594
1636
|
const nodeProxyArgs = hasStandardNodeProxyEnv() ? ['--use-env-proxy'] : [];
|
|
1595
|
-
const nodeProxyArgXml = nodeProxyArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
|
|
1596
|
-
const proxyEnvXml = buildLaunchdProxyEnvironmentXml();
|
|
1597
1637
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
1598
1638
|
fs.mkdirSync(configDir, { recursive: true });
|
|
1599
1639
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
1600
|
-
fs.writeFileSync(plist,
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
<dict>
|
|
1616
|
-
<key>PATH</key>
|
|
1617
|
-
<string>${xmlEscape(process.env.PATH || '')}</string>
|
|
1618
|
-
<key>HOME</key>
|
|
1619
|
-
<string>${xmlEscape(home)}</string>
|
|
1620
|
-
<key>USER</key>
|
|
1621
|
-
<string>${xmlEscape(process.env.USER || '')}</string>
|
|
1622
|
-
<key>LOGNAME</key>
|
|
1623
|
-
<string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
|
|
1624
|
-
<key>FOXCLAW_ENV</key>
|
|
1625
|
-
<string>${xmlEscape(envPath)}</string>
|
|
1626
|
-
${proxyEnvXml}
|
|
1627
|
-
</dict>
|
|
1628
|
-
<key>RunAtLoad</key>
|
|
1629
|
-
<true/>
|
|
1630
|
-
<key>KeepAlive</key>
|
|
1631
|
-
<true/>
|
|
1632
|
-
<key>StandardOutPath</key>
|
|
1633
|
-
<string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.out.log'))}</string>
|
|
1634
|
-
<key>StandardErrorPath</key>
|
|
1635
|
-
<string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.err.log'))}</string>
|
|
1636
|
-
</dict>
|
|
1637
|
-
</plist>
|
|
1638
|
-
`);
|
|
1640
|
+
fs.writeFileSync(plist, buildFoxclawLaunchdPlistText({
|
|
1641
|
+
label: 'app.foxden.foxclaw',
|
|
1642
|
+
nodePath: process.execPath,
|
|
1643
|
+
nodeArgs: nodeProxyArgs,
|
|
1644
|
+
entryPoint,
|
|
1645
|
+
workingDirectory: configDir,
|
|
1646
|
+
pathValue: process.env.PATH || '',
|
|
1647
|
+
home,
|
|
1648
|
+
user: process.env.USER || '',
|
|
1649
|
+
logname: process.env.LOGNAME || process.env.USER || '',
|
|
1650
|
+
envPath,
|
|
1651
|
+
proxyEnv: launchdProxyEnvironment(),
|
|
1652
|
+
stdoutPath: path.join(APP_HOME, 'logs', 'launchd.out.log'),
|
|
1653
|
+
stderrPath: path.join(APP_HOME, 'logs', 'launchd.err.log'),
|
|
1654
|
+
}));
|
|
1639
1655
|
spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
|
|
1640
1656
|
spawnChecked('launchctl', ['load', plist]);
|
|
1641
1657
|
console.log(`Installed ${plist}`);
|
|
@@ -1648,7 +1664,7 @@ function stopLaunchd() {
|
|
|
1648
1664
|
console.error('launchd stop is only available on macOS');
|
|
1649
1665
|
process.exit(1);
|
|
1650
1666
|
}
|
|
1651
|
-
const plist =
|
|
1667
|
+
const plist = launchdPlistPath();
|
|
1652
1668
|
if (!fs.existsSync(plist)) {
|
|
1653
1669
|
console.error(`launchd plist not found: ${plist}`);
|
|
1654
1670
|
process.exit(1);
|
|
@@ -1656,6 +1672,19 @@ function stopLaunchd() {
|
|
|
1656
1672
|
spawnChecked('launchctl', ['unload', plist]);
|
|
1657
1673
|
console.log(`Stopped ${plist}`);
|
|
1658
1674
|
}
|
|
1675
|
+
function uninstallLaunchd() {
|
|
1676
|
+
if (process.platform !== 'darwin') {
|
|
1677
|
+
console.error('launchd uninstall is only available on macOS');
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
const plist = launchdPlistPath();
|
|
1681
|
+
spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
|
|
1682
|
+
fs.rmSync(plist, { force: true });
|
|
1683
|
+
console.log(`Removed ${plist}`);
|
|
1684
|
+
}
|
|
1685
|
+
function launchdPlistPath() {
|
|
1686
|
+
return path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
|
|
1687
|
+
}
|
|
1659
1688
|
function buildServicePath(nodeDir) {
|
|
1660
1689
|
const pnpmPath = resolveCommand('pnpm');
|
|
1661
1690
|
const inferredPnpmHome = inferPnpmHomeFromEntryPoint(entryPoint) || '';
|
|
@@ -1716,16 +1745,15 @@ function hasStandardNodeProxyEnv() {
|
|
|
1716
1745
|
function proxyEnvValue(key) {
|
|
1717
1746
|
return process.env[key]?.trim() || '';
|
|
1718
1747
|
}
|
|
1719
|
-
function
|
|
1720
|
-
const entries =
|
|
1748
|
+
function launchdProxyEnvironment() {
|
|
1749
|
+
const entries = {};
|
|
1721
1750
|
for (const key of PROXY_ENV_KEYS) {
|
|
1722
1751
|
const value = proxyEnvValue(key);
|
|
1723
1752
|
if (!value)
|
|
1724
1753
|
continue;
|
|
1725
|
-
entries
|
|
1726
|
-
entries.push(` <string>${xmlEscape(value)}</string>`);
|
|
1754
|
+
entries[key] = value;
|
|
1727
1755
|
}
|
|
1728
|
-
return entries
|
|
1756
|
+
return entries;
|
|
1729
1757
|
}
|
|
1730
1758
|
function spawnChecked(commandName, args) {
|
|
1731
1759
|
const result = spawnSync(commandName, args, { stdio: 'inherit' });
|
|
@@ -1739,14 +1767,6 @@ function systemdEscape(value) {
|
|
|
1739
1767
|
function systemdUnescape(value) {
|
|
1740
1768
|
return value.replace(/\\x20/g, ' ').replace(/\\\\/g, '\\');
|
|
1741
1769
|
}
|
|
1742
|
-
function xmlEscape(value) {
|
|
1743
|
-
return value
|
|
1744
|
-
.replace(/&/g, '&')
|
|
1745
|
-
.replace(/</g, '<')
|
|
1746
|
-
.replace(/>/g, '>')
|
|
1747
|
-
.replace(/"/g, '"')
|
|
1748
|
-
.replace(/'/g, ''');
|
|
1749
|
-
}
|
|
1750
1770
|
async function runWeixinLoginCli() {
|
|
1751
1771
|
const [{ attachIlinkRuntimeFromBridgeLogger }, { startWeixinLoginWithQr, waitForWeixinLogin }, { accountFilePath, saveWeixinAccount }, { Logger },] = await Promise.all([
|
|
1752
1772
|
import('./channels/weixin/ilink/runtime_attach.js'),
|
|
@@ -48,6 +48,7 @@ Tasks:
|
|
|
48
48
|
8. Ask me to send /help, /status, and /auth privately to each configured Telegram bot; confirm /auth names that bot runtime.
|
|
49
49
|
9. Verify the final state:
|
|
50
50
|
- foxclaw.service is active/enabled on Linux
|
|
51
|
+
- the app.foxden.foxclaw launchd job is loaded on macOS, with no startup errors in ~/.foxclaw/logs/launchd.err.log
|
|
51
52
|
- foxclaw status works
|
|
52
53
|
10. Report the commands used, the final status, and the log command I should use if something stops working. Redact TG_BOT_TOKENS and never print the full token or full .env content.
|
|
53
54
|
11. If multiple bots are enabled, confirm foxclaw status lists independent app-servers; group-chat tests must mention or reply to the intended bot.
|
|
@@ -263,7 +263,12 @@ If install reports that linger could not be enabled automatically, run:
|
|
|
263
263
|
sudo loginctl enable-linger "$USER"
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
-
On macOS, `foxclaw start` manages launchd and starts FoxClaw when you log in.
|
|
266
|
+
On macOS, `foxclaw start` manages launchd and starts FoxClaw when you log in. Check state and startup logs with:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
270
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
271
|
+
```
|
|
267
272
|
|
|
268
273
|
For foreground debugging, stop the service first and then run `foxclaw serve`.
|
|
269
274
|
|
|
@@ -275,7 +280,7 @@ Check current status:
|
|
|
275
280
|
foxclaw status
|
|
276
281
|
```
|
|
277
282
|
|
|
278
|
-
Restart
|
|
283
|
+
Restart after changing `.env`:
|
|
279
284
|
|
|
280
285
|
```bash
|
|
281
286
|
foxclaw restart
|
|
@@ -293,6 +298,12 @@ Uninstall Linux service:
|
|
|
293
298
|
foxclaw uninstall-systemd
|
|
294
299
|
```
|
|
295
300
|
|
|
301
|
+
Uninstall macOS launchd service:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
foxclaw uninstall-launchd
|
|
305
|
+
```
|
|
306
|
+
|
|
296
307
|
Update FoxClaw later:
|
|
297
308
|
|
|
298
309
|
```bash
|
package/docs/troubleshooting.md
CHANGED
|
@@ -14,6 +14,13 @@ systemctl --user status foxclaw.service
|
|
|
14
14
|
journalctl --user -u foxclaw.service -f
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
If FoxClaw is installed as a macOS launchd service, check:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
21
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
## Doctor Failures
|
|
18
25
|
|
|
19
26
|
| Symptom | Meaning | Fix |
|
|
@@ -136,6 +143,13 @@ systemctl --user is-active foxclaw.service
|
|
|
136
143
|
pgrep -af foxclaw
|
|
137
144
|
```
|
|
138
145
|
|
|
146
|
+
On macOS, use:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
150
|
+
pgrep -af foxclaw
|
|
151
|
+
```
|
|
152
|
+
|
|
139
153
|
Stop the extra process or service, then restart FoxClaw:
|
|
140
154
|
|
|
141
155
|
```bash
|
|
@@ -200,12 +214,18 @@ When multiple bots share a group, unaddressed messages intentionally do not trig
|
|
|
200
214
|
|
|
201
215
|
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.
|
|
202
216
|
|
|
203
|
-
A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file.
|
|
217
|
+
A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. On Linux, check the env file installed into the service:
|
|
204
218
|
|
|
205
219
|
```bash
|
|
206
220
|
systemctl --user cat foxclaw.service
|
|
207
221
|
```
|
|
208
222
|
|
|
223
|
+
On macOS, inspect `FOXCLAW_ENV` in the launchd plist:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
plutil -p ~/Library/LaunchAgents/app.foxden.foxclaw.plist | grep FOXCLAW_ENV -A1
|
|
227
|
+
```
|
|
228
|
+
|
|
209
229
|
`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.
|
|
210
230
|
|
|
211
231
|
Make sure the file referenced by `Environment=FOXCLAW_ENV=...` contains your proxy variables, for example:
|
|
@@ -243,6 +263,8 @@ FoxClaw writes proxychains into the main service and removes stale FoxClaw `Exec
|
|
|
243
263
|
|
|
244
264
|
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.
|
|
245
265
|
|
|
266
|
+
macOS launchd follows the same rule: the plist records the absolute Node path that ran `foxclaw start` and does not rely on login-shell initialization.
|
|
267
|
+
|
|
246
268
|
If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24+ shell. Example for nvm users:
|
|
247
269
|
|
|
248
270
|
```bash
|
|
@@ -251,7 +273,15 @@ foxclaw start
|
|
|
251
273
|
systemctl --user status foxclaw.service
|
|
252
274
|
```
|
|
253
275
|
|
|
254
|
-
|
|
276
|
+
On macOS:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
nvm use 24
|
|
280
|
+
foxclaw start
|
|
281
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The status output should show a Node 24+ path. `foxclaw doctor` also checks the installed systemd/launchd service Node path and warns if it is missing or older than 24.
|
|
255
285
|
|
|
256
286
|
## Does It Run After Reboot?
|
|
257
287
|
|
package/docs/user-manual.md
CHANGED
|
@@ -145,7 +145,14 @@ systemctl --user status foxclaw.service
|
|
|
145
145
|
journalctl --user -u foxclaw.service -f
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
On macOS, `foxclaw start` manages launchd.
|
|
148
|
+
On macOS, `foxclaw start` manages launchd. Check launchd state and startup logs with:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
152
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For foreground debugging, stop the background service and run:
|
|
149
156
|
|
|
150
157
|
```bash
|
|
151
158
|
foxclaw stop
|
|
@@ -47,6 +47,7 @@ DEFAULT_CWD=<把绝对工作目录粘贴在这里>
|
|
|
47
47
|
8. 让我在每个已配置的 Telegram bot 私聊里发送 /help、/status 和 /auth;确认 /auth 显示对应 bot runtime。
|
|
48
48
|
9. 验证最终状态:
|
|
49
49
|
- Linux 上 foxclaw.service 处于 active/enabled
|
|
50
|
+
- macOS 上 app.foxden.foxclaw launchd job 已加载,并且 ~/.foxclaw/logs/launchd.err.log 没有启动错误
|
|
50
51
|
- foxclaw status 可以正常输出
|
|
51
52
|
10. 汇报执行过的命令、最终状态和后续看日志的命令。请隐藏 TG_BOT_TOKENS,不要打印完整 token 或完整 .env。
|
|
52
53
|
11. 如果启用了多个 bot,确认 foxclaw status 列出独立 app-server;群聊测试必须点名或回复目标 bot。
|
|
@@ -261,7 +261,12 @@ journalctl --user -u foxclaw.service -f
|
|
|
261
261
|
sudo loginctl enable-linger "$USER"
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
macOS 上 `foxclaw start` 管理 launchd,并在你登录后启动 FoxClaw
|
|
264
|
+
macOS 上 `foxclaw start` 管理 launchd,并在你登录后启动 FoxClaw。查看状态和启动日志:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
268
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
269
|
+
```
|
|
265
270
|
|
|
266
271
|
前台调试时,先停后台服务,再运行 `foxclaw serve`。
|
|
267
272
|
|
|
@@ -291,6 +296,12 @@ foxclaw stop
|
|
|
291
296
|
foxclaw uninstall-systemd
|
|
292
297
|
```
|
|
293
298
|
|
|
299
|
+
卸载 macOS launchd 服务:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
foxclaw uninstall-launchd
|
|
303
|
+
```
|
|
304
|
+
|
|
294
305
|
以后升级 FoxClaw:
|
|
295
306
|
|
|
296
307
|
```bash
|
|
@@ -14,6 +14,13 @@ systemctl --user status foxclaw.service
|
|
|
14
14
|
journalctl --user -u foxclaw.service -f
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
如果 FoxClaw 是 macOS launchd 服务,再看:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
21
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
## Doctor 检查失败
|
|
18
25
|
|
|
19
26
|
| 现象 | 含义 | 处理方式 |
|
|
@@ -137,6 +144,13 @@ systemctl --user is-active foxclaw.service
|
|
|
137
144
|
pgrep -af foxclaw
|
|
138
145
|
```
|
|
139
146
|
|
|
147
|
+
macOS 上用:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
151
|
+
pgrep -af foxclaw
|
|
152
|
+
```
|
|
153
|
+
|
|
140
154
|
停掉多余进程或服务后,重启 FoxClaw:
|
|
141
155
|
|
|
142
156
|
```bash
|
|
@@ -201,12 +215,18 @@ tail -f ~/.foxclaw/logs/codex-app-server-bot<id>.log
|
|
|
201
215
|
|
|
202
216
|
如果 Telegram 里看到 `ChatGPT backend 403 Forbidden`,或者 app-server 日志里出现 `Unable to load site`、`cf-ray`、`chatgpt.com/backend-api`,通常不是 `auth.json` 文件坏了,而是服务进程访问 ChatGPT 后端时没有走正确网络。
|
|
203
217
|
|
|
204
|
-
常见原因是:你在 shell 里配置了代理,或者项目 `.env` 里有代理,但 systemd/launchd 服务实际读的是另一个 env
|
|
218
|
+
常见原因是:你在 shell 里配置了代理,或者项目 `.env` 里有代理,但 systemd/launchd 服务实际读的是另一个 env 文件。Linux 先看服务用的是哪个 env:
|
|
205
219
|
|
|
206
220
|
```bash
|
|
207
221
|
systemctl --user cat foxclaw.service
|
|
208
222
|
```
|
|
209
223
|
|
|
224
|
+
macOS 看 launchd plist 里的 `FOXCLAW_ENV`:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
plutil -p ~/Library/LaunchAgents/app.foxden.foxclaw.plist | grep FOXCLAW_ENV -A1
|
|
228
|
+
```
|
|
229
|
+
|
|
210
230
|
`foxclaw init` 会检测当前 shell 里的代理环境变量,并询问是否保存到 FoxClaw `.env`。如果你跳过了这一步,`foxclaw doctor` 会在发现“shell 有代理,但 FoxClaw env 没有代理”时给出 `[WARN]`。
|
|
211
231
|
|
|
212
232
|
确认 `Environment=FOXCLAW_ENV=...` 指向的文件里有你的代理配置,例如:
|
|
@@ -244,6 +264,8 @@ FoxClaw 会把 proxychains 写进主 service,并清理旧的 FoxClaw `ExecStar
|
|
|
244
264
|
|
|
245
265
|
systemd 安装脚本会记录当时正在运行的 Node 绝对路径,不依赖 systemd 去加载 `nvm.sh` 或其它 shell 初始化脚本。无论你用 nvm、fnm、asdf、mise、Volta、Homebrew 还是系统 Node,原则都是:从 Node 24+ 的 shell 里执行 `foxclaw start`,服务之后就固定使用这个 Node 24+ 路径。
|
|
246
266
|
|
|
267
|
+
macOS launchd 也是同样原则:plist 会记录执行 `foxclaw start` 时的 Node 绝对路径,不依赖登录 shell 初始化脚本。
|
|
268
|
+
|
|
247
269
|
如果你从 Node 22 或更旧版本的 shell 里安装过服务,请从 Node 24+ 的 shell 重新安装。nvm 用户示例:
|
|
248
270
|
|
|
249
271
|
```bash
|
|
@@ -252,7 +274,15 @@ foxclaw start
|
|
|
252
274
|
systemctl --user status foxclaw.service
|
|
253
275
|
```
|
|
254
276
|
|
|
255
|
-
|
|
277
|
+
macOS 用户示例:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
nvm use 24
|
|
281
|
+
foxclaw start
|
|
282
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
状态输出里应该能看到 Node 24+ 的路径。`foxclaw doctor` 也会检查已安装 systemd/launchd 服务里的 Node 路径,如果发现路径不存在或版本低于 24,会提示重新运行 `foxclaw start`。
|
|
256
286
|
|
|
257
287
|
## 重启后是否会自动运行
|
|
258
288
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -145,7 +145,14 @@ systemctl --user status foxclaw.service
|
|
|
145
145
|
journalctl --user -u foxclaw.service -f
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
macOS 上 `foxclaw start` 会管理 launchd
|
|
148
|
+
macOS 上 `foxclaw start` 会管理 launchd。查看 launchd 状态和启动日志:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
152
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
前台排障时,先停后台服务,再运行:
|
|
149
156
|
|
|
150
157
|
```bash
|
|
151
158
|
foxclaw stop
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxden-app/foxclaw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"install-systemd": "node dist/main.js install-systemd",
|
|
49
49
|
"uninstall-systemd": "node dist/main.js uninstall-systemd",
|
|
50
50
|
"install-launchd": "node dist/main.js install-launchd",
|
|
51
|
+
"uninstall-launchd": "node dist/main.js uninstall-launchd",
|
|
51
52
|
"typecheck": "tsc --noEmit",
|
|
52
53
|
"lint": "eslint .",
|
|
53
54
|
"test": "node --test --import tsx $(find src -name '*.test.ts' -print)",
|
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -144,8 +144,8 @@ Use `--no-start` by default when preparing a second Mac before a unique bot toke
|
|
|
144
144
|
|
|
145
145
|
After either bootstrap path:
|
|
146
146
|
|
|
147
|
-
1. Run `node dist/main.js doctor` in the installed bridge repo.
|
|
148
|
-
2. If launchd was installed, run `node dist/main.js status
|
|
147
|
+
1. Run `node dist/main.js doctor` in the installed bridge repo.
|
|
148
|
+
2. If launchd was installed, run `node dist/main.js status`, `launchctl print "gui/$(id -u)/app.foxden.foxclaw"`, and inspect `~/.foxclaw/logs/launchd.err.log` if either status check fails.
|
|
149
149
|
3. Check `codex login status`. If authentication is missing, stop and tell the user exactly how to log in.
|
|
150
150
|
4. If the bridge is expected to answer in a Telegram group, confirm:
|
|
151
151
|
- `privacy mode` is disabled
|