@foxden-app/foxclaw 0.13.0 → 0.13.1
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 +14 -0
- package/dist/bridge/unified_bridge.js +3 -3
- package/dist/update.d.ts +27 -2
- package/dist/update.js +81 -28
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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.13.1 - 2026-10-06
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
|
|
9
|
+
- **修复 `/update` 意外降级**:pnpm 10 与 pnpm 11 安装并存时,以本次安装的全局目录选择启动入口,禁止误用另一目录中的旧启动脚本。
|
|
10
|
+
- 修复升级完成卡片丢失 Antigravity CLI 实际版本、回退显示升级前版本的问题。
|
|
11
|
+
- **升级前后双重版本校验**:从官方仓库获取目标版本并按精确版本安装;拒绝降级,安装版本必须与目标一致才允许更新其他 CLI 和重启服务。校验失败不会发送升级成功或集群升级广播。
|
|
12
|
+
|
|
13
|
+
### English
|
|
14
|
+
|
|
15
|
+
- **Fixed accidental downgrades during `/update`**: when pnpm 10 and pnpm 11 installations coexist, resolve the entry point from the installer's global root instead of an older launcher from another layout.
|
|
16
|
+
- Preserve the actual Antigravity CLI update versions in completion cards instead of falling back to the pre-update version.
|
|
17
|
+
- **Verify versions before installation and restart**: fetch the target from the official registry and install that exact version. Reject downgrades and require the installed version to match before updating other CLIs or restarting. Failed checks never report success or broadcast a cluster update.
|
|
18
|
+
|
|
5
19
|
## 0.13.0 - 2026-10-06
|
|
6
20
|
|
|
7
21
|
### 中文
|
|
@@ -382,9 +382,9 @@ export class UnifiedBridgeCore {
|
|
|
382
382
|
const title = isZh ? '🎉 FoxClaw 全链路升级完成' : '🎉 FoxClaw Update Completed';
|
|
383
383
|
const lines = [
|
|
384
384
|
`**${title}**`,
|
|
385
|
-
`• FoxClaw: \`${status.fromVersion}\` ➔ \`${status.toVersion ?? '
|
|
386
|
-
`• Codex CLI: \`${status.codexFromVersion ?? '
|
|
387
|
-
`• Antigravity CLI: \`${status.agyFromVersion ?? '
|
|
385
|
+
`• FoxClaw: \`${status.fromVersion}\` ➔ \`${status.toVersion ?? 'unknown'}\``,
|
|
386
|
+
`• Codex CLI: \`${status.codexFromVersion ?? 'unknown'}\` ➔ \`${status.codexToVersion ?? 'unknown'}\``,
|
|
387
|
+
`• Antigravity CLI: \`${status.agyFromVersion ?? 'unknown'}\` ➔ \`${status.agyToVersion ?? 'unknown'}\``,
|
|
388
388
|
];
|
|
389
389
|
if (notes.length > 0) {
|
|
390
390
|
lines.push('', isZh ? '**更新日志**:' : '**Release Notes**:');
|
package/dist/update.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface PendingClusterUpdateBroadcast {
|
|
|
68
68
|
fromVersion: string;
|
|
69
69
|
updatedAt: string;
|
|
70
70
|
}
|
|
71
|
+
interface CodexCliUpdateResult {
|
|
72
|
+
message: string;
|
|
73
|
+
fromVersion: string | null;
|
|
74
|
+
toVersion: string | null;
|
|
75
|
+
}
|
|
71
76
|
export declare function selfUpdateStatusPath(statusPath: string): string;
|
|
72
77
|
export declare function inferPnpmHomeFromEntryPoint(entryPoint: string): string | null;
|
|
73
78
|
export declare function inferPnpmFallbackPackageFromEntryPoint(entryPoint: string): string;
|
|
@@ -88,11 +93,31 @@ export declare function buildSelfUpdateLaunchCommand(options: {
|
|
|
88
93
|
systemdRunPath?: string | null;
|
|
89
94
|
unitName?: string;
|
|
90
95
|
}): SelfUpdateLaunchCommand;
|
|
91
|
-
export declare function
|
|
96
|
+
export declare function assertSelfUpdateVersion(current: string, candidate: string, expected?: string): void;
|
|
97
|
+
interface SelfUpdateOperations {
|
|
98
|
+
latestVersion: (installer: SelfUpdateInstaller, env: NodeJS.ProcessEnv) => string;
|
|
99
|
+
run: typeof runInherited;
|
|
100
|
+
entryPoint: typeof resolveUpdatedEntryPoint;
|
|
101
|
+
readVersion: typeof readInstalledPackageVersion;
|
|
102
|
+
updateCodex: typeof updateManagedCodexCli;
|
|
103
|
+
updateAgy: typeof updateManagedAgyCli;
|
|
104
|
+
}
|
|
105
|
+
export declare function performSelfUpdate(options: PerformSelfUpdateOptions, overrides?: Partial<SelfUpdateOperations>): SelfUpdateOutcome;
|
|
92
106
|
export declare function readPendingClusterUpdateBroadcast(filePath: string): PendingClusterUpdateBroadcast | null;
|
|
93
107
|
export declare function writePendingClusterUpdateBroadcast(filePath: string, value: PendingClusterUpdateBroadcast): void;
|
|
94
108
|
export declare function clearPendingClusterUpdateBroadcast(filePath: string): void;
|
|
109
|
+
declare function updateManagedCodexCli(codexCliBin: string, nodePath: string, env: NodeJS.ProcessEnv): CodexCliUpdateResult;
|
|
110
|
+
interface AgyCliUpdateResult {
|
|
111
|
+
message: string;
|
|
112
|
+
fromVersion: string | null;
|
|
113
|
+
toVersion: string | null;
|
|
114
|
+
}
|
|
115
|
+
declare function updateManagedAgyCli(agyCliBin: string, env: NodeJS.ProcessEnv): AgyCliUpdateResult;
|
|
116
|
+
declare function runInherited(command: string, args: string[], env: NodeJS.ProcessEnv): void;
|
|
95
117
|
export declare function resolveFoxclawEntryPointFromGlobalRoot(globalRoot: string, exists?: (target: string) => boolean): string | null;
|
|
96
|
-
export declare function resolveFoxclawEntryPointFromPnpmHome(pnpmHome: string, preferBin?: boolean, exists?: (target: string) => boolean, readText?: (target: string) => string): string | null;
|
|
118
|
+
export declare function resolveFoxclawEntryPointFromPnpmHome(pnpmHome: string, preferBin?: boolean, exists?: (target: string) => boolean, readText?: (target: string) => string, installationRoot?: string): string | null;
|
|
119
|
+
export declare function resolveFoxclawEntryPointFromInstallation(globalRoot: string, pnpmHome?: string, exists?: (target: string) => boolean, readText?: (target: string) => string): string | null;
|
|
120
|
+
declare function resolveUpdatedEntryPoint(installer: SelfUpdateInstaller, env: NodeJS.ProcessEnv): string;
|
|
121
|
+
declare function readInstalledPackageVersion(updatedEntryPoint: string): string;
|
|
97
122
|
export declare function extractReleaseNotes(changelog: string, version: string, locale: AppLocale): string[] | null;
|
|
98
123
|
export {};
|
package/dist/update.js
CHANGED
|
@@ -3,7 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
-
|
|
6
|
+
import { valid, lt } from 'semver';
|
|
7
|
+
const PACKAGE_NAME = '@foxden-app/foxclaw';
|
|
8
|
+
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
9
|
+
const PACKAGE_REGISTRY = 'https://registry.npmjs.org';
|
|
7
10
|
const CODEX_PACKAGE_SPEC = '@openai/codex@latest';
|
|
8
11
|
const UPDATE_STATUS_FILENAME = 'self-update.json';
|
|
9
12
|
export const SELF_UPDATE_PENDING_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -153,6 +156,9 @@ export function readSelfUpdateStatus(statusFile) {
|
|
|
153
156
|
...(typeof parsed.codexUpdate === 'string' ? { codexUpdate: parsed.codexUpdate } : {}),
|
|
154
157
|
...(typeof parsed.codexFromVersion === 'string' ? { codexFromVersion: parsed.codexFromVersion } : {}),
|
|
155
158
|
...(typeof parsed.codexToVersion === 'string' ? { codexToVersion: parsed.codexToVersion } : {}),
|
|
159
|
+
...(typeof parsed.agyUpdate === 'string' ? { agyUpdate: parsed.agyUpdate } : {}),
|
|
160
|
+
...(typeof parsed.agyFromVersion === 'string' ? { agyFromVersion: parsed.agyFromVersion } : {}),
|
|
161
|
+
...(typeof parsed.agyToVersion === 'string' ? { agyToVersion: parsed.agyToVersion } : {}),
|
|
156
162
|
error: typeof parsed.error === 'string' ? parsed.error : null,
|
|
157
163
|
updatedAt: parsed.updatedAt,
|
|
158
164
|
};
|
|
@@ -311,25 +317,73 @@ export function buildSelfUpdateLaunchCommand(options) {
|
|
|
311
317
|
viaSystemdRun: false,
|
|
312
318
|
};
|
|
313
319
|
}
|
|
314
|
-
export function
|
|
320
|
+
export function assertSelfUpdateVersion(current, candidate, expected) {
|
|
321
|
+
const from = valid(current);
|
|
322
|
+
const to = valid(candidate);
|
|
323
|
+
if (!from || !to) {
|
|
324
|
+
throw new Error(`Cannot verify FoxClaw update versions: ${current} -> ${candidate}.`);
|
|
325
|
+
}
|
|
326
|
+
if (lt(to, from)) {
|
|
327
|
+
throw new Error(`Refusing FoxClaw downgrade: ${current} -> ${candidate}.`);
|
|
328
|
+
}
|
|
329
|
+
if (expected && to !== valid(expected)) {
|
|
330
|
+
throw new Error(`Installed FoxClaw ${candidate} does not match requested ${expected}; service restart refused.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function readLatestFoxclawVersion(installer, env) {
|
|
334
|
+
// Read the same registry used by the exact-version install, bypassing stale local metadata.
|
|
335
|
+
const result = spawnSync(installer.command, ['view', PACKAGE_SPEC, 'version', '--json', `--registry=${PACKAGE_REGISTRY}`, '--prefer-online'], { encoding: 'utf8', env, timeout: 30_000 });
|
|
336
|
+
if (result.error)
|
|
337
|
+
throw result.error;
|
|
338
|
+
if (result.status !== 0)
|
|
339
|
+
throw new Error('Could not read the latest FoxClaw version from the npm registry.');
|
|
340
|
+
let version;
|
|
341
|
+
try {
|
|
342
|
+
version = JSON.parse(result.stdout.trim());
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
throw new Error('Invalid latest FoxClaw version response.');
|
|
346
|
+
}
|
|
347
|
+
if (typeof version !== 'string' || !valid(version))
|
|
348
|
+
throw new Error('Invalid latest FoxClaw version response.');
|
|
349
|
+
return valid(version);
|
|
350
|
+
}
|
|
351
|
+
export function performSelfUpdate(options, overrides = {}) {
|
|
315
352
|
const env = options.env ?? process.env;
|
|
353
|
+
const operations = {
|
|
354
|
+
latestVersion: readLatestFoxclawVersion,
|
|
355
|
+
run: runInherited,
|
|
356
|
+
entryPoint: resolveUpdatedEntryPoint,
|
|
357
|
+
readVersion: readInstalledPackageVersion,
|
|
358
|
+
updateCodex: updateManagedCodexCli,
|
|
359
|
+
updateAgy: updateManagedAgyCli,
|
|
360
|
+
...overrides,
|
|
361
|
+
};
|
|
316
362
|
let toVersion = null;
|
|
317
363
|
let codexUpdate = null;
|
|
318
364
|
let agyUpdate = null;
|
|
319
365
|
try {
|
|
320
|
-
codexUpdate = updateManagedCodexCli(options.codexCliBin ?? env.CODEX_CLI_BIN ?? '', options.nodePath, env);
|
|
321
|
-
console.log(`[UPDATE] ${codexUpdate.message}`);
|
|
322
|
-
agyUpdate = updateManagedAgyCli(options.agyCliBin ?? env.AGY_CLI_BIN ?? '', env);
|
|
323
|
-
console.log(`[UPDATE] ${agyUpdate.message}`);
|
|
324
366
|
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath, fs.existsSync, env);
|
|
325
367
|
const installerEnv = buildInstallerEnv(options.entryPoint, installer, env);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
368
|
+
const targetVersion = operations.latestVersion(installer, installerEnv);
|
|
369
|
+
assertSelfUpdateVersion(options.version, targetVersion);
|
|
370
|
+
const spec = `${PACKAGE_NAME}@${targetVersion}`;
|
|
371
|
+
const installArgs = [
|
|
372
|
+
...installer.installArgs.map(arg => arg === PACKAGE_SPEC ? spec : arg),
|
|
373
|
+
`--registry=${PACKAGE_REGISTRY}`,
|
|
374
|
+
];
|
|
375
|
+
console.log(`[UPDATE] Installing ${spec} with ${installer.manager}...`);
|
|
376
|
+
operations.run(installer.command, installArgs, installerEnv);
|
|
377
|
+
const updatedEntryPoint = operations.entryPoint(installer, installerEnv);
|
|
378
|
+
toVersion = operations.readVersion(updatedEntryPoint);
|
|
379
|
+
assertSelfUpdateVersion(options.version, toVersion, targetVersion);
|
|
380
|
+
codexUpdate = operations.updateCodex(options.codexCliBin ?? env.CODEX_CLI_BIN ?? '', options.nodePath, env);
|
|
381
|
+
console.log(`[UPDATE] ${codexUpdate.message}`);
|
|
382
|
+
agyUpdate = operations.updateAgy(options.agyCliBin ?? env.AGY_CLI_BIN ?? '', env);
|
|
383
|
+
console.log(`[UPDATE] ${agyUpdate.message}`);
|
|
330
384
|
const releaseNotes = readInstalledReleaseNotes(updatedEntryPoint, toVersion, options.notificationFile);
|
|
331
385
|
console.log('[UPDATE] Running checks and restarting the FoxClaw service...');
|
|
332
|
-
|
|
386
|
+
operations.run(options.nodePath, [updatedEntryPoint, 'start'], installerEnv);
|
|
333
387
|
completeNotification(options.notificationFile, 'succeeded', toVersion, codexUpdate, null, releaseNotes, agyUpdate);
|
|
334
388
|
if (options.clusterBroadcastFile && env.FOXCLAW_SUPPRESS_UPDATE_BROADCAST !== '1') {
|
|
335
389
|
writePendingClusterUpdateBroadcast(options.clusterBroadcastFile, {
|
|
@@ -339,23 +393,13 @@ export function performSelfUpdate(options) {
|
|
|
339
393
|
});
|
|
340
394
|
}
|
|
341
395
|
console.log(`[OK] FoxClaw updated and restarted: ${options.version} -> ${toVersion}`);
|
|
342
|
-
return {
|
|
343
|
-
ok: true,
|
|
344
|
-
fromVersion: options.version,
|
|
345
|
-
toVersion,
|
|
346
|
-
error: null,
|
|
347
|
-
};
|
|
396
|
+
return { ok: true, fromVersion: options.version, toVersion, error: null };
|
|
348
397
|
}
|
|
349
398
|
catch (error) {
|
|
350
399
|
const message = formatError(error);
|
|
351
400
|
completeNotification(options.notificationFile, 'failed', toVersion, codexUpdate, message, null, agyUpdate);
|
|
352
401
|
console.error(`[FAIL] FoxClaw update failed: ${message}`);
|
|
353
|
-
return {
|
|
354
|
-
ok: false,
|
|
355
|
-
fromVersion: options.version,
|
|
356
|
-
toVersion,
|
|
357
|
-
error: message,
|
|
358
|
-
};
|
|
402
|
+
return { ok: false, fromVersion: options.version, toVersion, error: message };
|
|
359
403
|
}
|
|
360
404
|
}
|
|
361
405
|
export function readPendingClusterUpdateBroadcast(filePath) {
|
|
@@ -563,7 +607,7 @@ export function resolveFoxclawEntryPointFromGlobalRoot(globalRoot, exists = fs.e
|
|
|
563
607
|
];
|
|
564
608
|
return candidates.find(candidate => exists(candidate)) ?? null;
|
|
565
609
|
}
|
|
566
|
-
export function resolveFoxclawEntryPointFromPnpmHome(pnpmHome, preferBin = false, exists = fs.existsSync, readText = (target) => fs.readFileSync(target, 'utf8')) {
|
|
610
|
+
export function resolveFoxclawEntryPointFromPnpmHome(pnpmHome, preferBin = false, exists = fs.existsSync, readText = (target) => fs.readFileSync(target, 'utf8'), installationRoot) {
|
|
567
611
|
const commandName = process.platform === 'win32' ? 'foxclaw.cmd' : 'foxclaw';
|
|
568
612
|
const rootShim = path.join(pnpmHome, commandName);
|
|
569
613
|
const binShim = path.join(pnpmHome, 'bin', commandName);
|
|
@@ -581,11 +625,23 @@ export function resolveFoxclawEntryPointFromPnpmHome(pnpmHome, preferBin = false
|
|
|
581
625
|
const matches = contents.match(/(?:[A-Za-z]:[\\/]|\/)[^\r\n"']*?[\\/]node_modules[\\/]@foxden-app[\\/]foxclaw[\\/]dist[\\/]main\.js/g);
|
|
582
626
|
const entryPoint = matches?.at(-1) ?? null;
|
|
583
627
|
if (entryPoint && exists(entryPoint)) {
|
|
628
|
+
const relative = installationRoot ? path.relative(path.resolve(installationRoot), path.resolve(entryPoint)) : null;
|
|
629
|
+
if (relative !== null && (path.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path.sep}`))) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
584
632
|
return entryPoint;
|
|
585
633
|
}
|
|
586
634
|
}
|
|
587
635
|
return null;
|
|
588
636
|
}
|
|
637
|
+
export function resolveFoxclawEntryPointFromInstallation(globalRoot, pnpmHome, exists = fs.existsSync, readText = target => fs.readFileSync(target, 'utf8')) {
|
|
638
|
+
// The installer-owned root is authoritative. Another pnpm layout may have a stale shim.
|
|
639
|
+
const direct = resolveFoxclawEntryPointFromGlobalRoot(globalRoot, exists);
|
|
640
|
+
if (direct || !pnpmHome)
|
|
641
|
+
return direct;
|
|
642
|
+
const installationRoot = path.basename(globalRoot) === 'node_modules' ? path.dirname(globalRoot) : globalRoot;
|
|
643
|
+
return resolveFoxclawEntryPointFromPnpmHome(pnpmHome, /^v\d+$/.test(path.basename(globalRoot)), exists, readText, installationRoot);
|
|
644
|
+
}
|
|
589
645
|
function resolveUpdatedEntryPoint(installer, env) {
|
|
590
646
|
const result = spawnSync(installer.command, installer.rootArgs, { encoding: 'utf8', env });
|
|
591
647
|
if (result.error) {
|
|
@@ -598,10 +654,7 @@ function resolveUpdatedEntryPoint(installer, env) {
|
|
|
598
654
|
if (!globalRoot) {
|
|
599
655
|
throw new Error(`Could not locate the updated global package root using ${installer.manager}.`);
|
|
600
656
|
}
|
|
601
|
-
const updatedEntryPoint = (installer.pnpmHome
|
|
602
|
-
? resolveFoxclawEntryPointFromPnpmHome(installer.pnpmHome, /^v\d+$/.test(path.basename(globalRoot)))
|
|
603
|
-
: null)
|
|
604
|
-
?? resolveFoxclawEntryPointFromGlobalRoot(globalRoot);
|
|
657
|
+
const updatedEntryPoint = resolveFoxclawEntryPointFromInstallation(globalRoot, installer.pnpmHome);
|
|
605
658
|
if (!updatedEntryPoint) {
|
|
606
659
|
throw new Error(`Updated FoxClaw entry point was not found below ${globalRoot} (checked direct and node_modules layouts).`);
|
|
607
660
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxden-app/foxclaw",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Foxden local execution claw for controlling Codex, OpenCode, Antigravity, and DeepSeek Harness from trusted chat interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -58,11 +58,13 @@
|
|
|
58
58
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
59
59
|
"@opencode-ai/sdk": "1.18.15",
|
|
60
60
|
"dotenv": "^16.6.1",
|
|
61
|
-
"qrcode-terminal": "^0.12.0"
|
|
61
|
+
"qrcode-terminal": "^0.12.0",
|
|
62
|
+
"semver": "7.7.4"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@eslint/js": "^9.39.0",
|
|
65
66
|
"@types/node": "^24.12.0",
|
|
67
|
+
"@types/semver": "7.7.1",
|
|
66
68
|
"eslint": "^9.39.0",
|
|
67
69
|
"tsx": "^4.19.3",
|
|
68
70
|
"typescript": "5.9.3",
|