@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 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 ?? '0.11.0'}\``,
386
- `• Codex CLI: \`${status.codexFromVersion ?? '0.157.1'}\` ➔ \`${status.codexToVersion ?? '0.159.2'}\``,
387
- `• Antigravity CLI: \`${status.agyFromVersion ?? '1.2.14'}\` (最新)`,
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 performSelfUpdate(options: PerformSelfUpdateOptions): SelfUpdateOutcome;
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
- const PACKAGE_SPEC = '@foxden-app/foxclaw@latest';
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 performSelfUpdate(options) {
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
- console.log(`[UPDATE] Installing ${PACKAGE_SPEC} with ${installer.manager}...`);
327
- runInherited(installer.command, installer.installArgs, installerEnv);
328
- const updatedEntryPoint = resolveUpdatedEntryPoint(installer, installerEnv);
329
- toVersion = readInstalledPackageVersion(updatedEntryPoint);
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
- runInherited(options.nodePath, [updatedEntryPoint, 'start'], installerEnv);
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.0",
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",