diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ee126b2..5111ea119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,8 @@ Docs: https://docs.clawd.bot ### Changes - macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x. -- Models: add Kimi Code provider onboarding and docs. (#1085) — thanks @dan-dr. +- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg. -### Fixes -- Matrix: send voice/image-specific media payloads and keep legacy poll parsing. (#1088) — thanks @sibbl. -- Telegram: allow media-only message tool sends to request voice notes via `asVoice`. (#1099) — thanks @mukhtharcm. -- Discord: soften logs for expired interactions and stale component clicks. ## 2026.1.16-2 ### Changes diff --git a/apps/macos/Sources/Clawdbot/CLIInstaller.swift b/apps/macos/Sources/Clawdbot/CLIInstaller.swift index 41abb29e6..d967002f5 100644 --- a/apps/macos/Sources/Clawdbot/CLIInstaller.swift +++ b/apps/macos/Sources/Clawdbot/CLIInstaller.swift @@ -35,7 +35,7 @@ enum CLIInstaller { } static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async { - let expected = GatewayEnvironment.expectedGatewayVersion()?.description ?? "latest" + let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest" let prefix = Self.installPrefix() await statusHandler("Installing clawdbot CLI…") let cmd = self.installScriptCommand(version: expected, prefix: prefix) diff --git a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift index 6560b1870..032275bba 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift @@ -80,8 +80,13 @@ enum GatewayEnvironment { } static func expectedGatewayVersion() -> Semver? { + Semver.parse(self.expectedGatewayVersionString()) + } + + static func expectedGatewayVersionString() -> String? { let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - return Semver.parse(bundleVersion) + let trimmed = bundleVersion?.trimmingCharacters(in: .whitespacesAndNewlines) + return (trimmed?.isEmpty == false) ? trimmed : nil } // Exposed for tests so we can inject fake version checks without rewriting bundle metadata. @@ -100,6 +105,7 @@ enum GatewayEnvironment { } } let expected = self.expectedGatewayVersion() + let expectedString = self.expectedGatewayVersionString() let projectRoot = CommandResolver.projectRoot() let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot) @@ -110,8 +116,8 @@ enum GatewayEnvironment { kind: .missingNode, nodeVersion: nil, gatewayVersion: nil, - requiredGateway: expected?.description, - message: RuntimeLocator.describeFailure(err)) + requiredGateway: expectedString, + message: RuntimeLocator.describeFailure(err)) case let .success(runtime): let gatewayBin = CommandResolver.clawdbotExecutable() @@ -120,7 +126,7 @@ enum GatewayEnvironment { kind: .missingGateway, nodeVersion: runtime.version.description, gatewayVersion: nil, - requiredGateway: expected?.description, + requiredGateway: expectedString, message: "clawdbot CLI not found in PATH; install the CLI.") } @@ -128,13 +134,14 @@ enum GatewayEnvironment { ?? self.readLocalGatewayVersion(projectRoot: projectRoot) if let expected, let installed, !installed.compatible(with: expected) { + let expectedText = expectedString ?? expected.description return GatewayEnvironmentStatus( - kind: .incompatible(found: installed.description, required: expected.description), + kind: .incompatible(found: installed.description, required: expectedText), nodeVersion: runtime.version.description, gatewayVersion: installed.description, - requiredGateway: expected.description, + requiredGateway: expectedText, message: """ - Gateway version \(installed.description) is incompatible with app \(expected.description); + Gateway version \(installed.description) is incompatible with app \(expectedText); install or update the global package. """) } @@ -152,7 +159,7 @@ enum GatewayEnvironment { kind: .ok, nodeVersion: runtime.version.description, gatewayVersion: gatewayVersionText, - requiredGateway: expected?.description, + requiredGateway: expectedString, message: "Node \(runtime.version.description); gateway \(gatewayVersionText) \(gatewayLabelText)") } } @@ -220,8 +227,18 @@ enum GatewayEnvironment { } static func installGlobal(version: Semver?, statusHandler: @escaping @Sendable (String) -> Void) async { + await self.installGlobal(versionString: version?.description, statusHandler: statusHandler) + } + + static func installGlobal(versionString: String?, statusHandler: @escaping @Sendable (String) -> Void) async { let preferred = CommandResolver.preferredPaths().joined(separator: ":") - let target = version?.description ?? "latest" + let trimmed = versionString?.trimmingCharacters(in: .whitespacesAndNewlines) + let target: String + if let trimmed, !trimmed.isEmpty { + target = trimmed + } else { + target = "latest" + } let npm = CommandResolver.findExecutable(named: "npm") let pnpm = CommandResolver.findExecutable(named: "pnpm") let bun = CommandResolver.findExecutable(named: "bun") diff --git a/apps/macos/Sources/Clawdbot/Onboarding.swift b/apps/macos/Sources/Clawdbot/Onboarding.swift index 0af5e94e5..f45eee8ce 100644 --- a/apps/macos/Sources/Clawdbot/Onboarding.swift +++ b/apps/macos/Sources/Clawdbot/Onboarding.swift @@ -155,7 +155,7 @@ struct OnboardingView: View { var canAdvance: Bool { !self.isWizardBlocking } var devLinkCommand: String { - let version = GatewayEnvironment.expectedGatewayVersion()?.description ?? "latest" + let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest" return "npm install -g clawdbot@\(version)" } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift index 069e5b7fa..fb01ea638 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift @@ -44,6 +44,7 @@ import Testing @Test func expectedGatewayVersionFromStringUsesParser() { #expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2)) + #expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11)) #expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil) } } diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts index 4287c9c9e..05def80da 100644 --- a/src/auto-reply/reply/session-updates.test.ts +++ b/src/auto-reply/reply/session-updates.test.ts @@ -7,7 +7,7 @@ import { prependSystemEvents } from "./session-updates.js"; describe("prependSystemEvents", () => { it("adds a UTC timestamp to queued system events", async () => { vi.useFakeTimers(); - const timestamp = new Date("2026-01-12T20:19:17"); + const timestamp = new Date("2026-01-12T20:19:17Z"); vi.setSystemTime(timestamp); enqueueSystemEvent("Model switched.", { sessionKey: "agent:main:main" });