Files
zeekay acaeda400a Eradicate OpenClaw — resolve merge conflicts and finish the rebrand
Three coordinated things:

1. Resolve the committed upstream/main merge conflicts. 94 files were
   sitting with raw <<<<<<< HEAD ... >>>>>>> upstream/main blocks. Every
   conflict had HanzoBot/Bot on HEAD and OpenClaw on upstream. Resolved
   by keeping HEAD everywhere — 540 conflict blocks.

2. Rewrite remaining OpenClaw text refs.
     OPENCLAW   → HANZO_BOT
     OpenClaw   → HanzoBot          (PascalCase type identifiers)
     openclaw   → bot               (codebase convention — botToken,
                                    botUsername etc.; avoids invalid
                                    hyphenated JS identifiers)
     openclaw[_]/openclaw[A-Z] → bot[_]/bot[A-Z]   (compound)
     ai.openclaw.x → ai.hanzo.bot.x  (JVM package path)

3. Delete or rename openclaw-named file paths (116 of them). Dead
   duplicates with a Bot-named canonical from the partial migration:
   deleted. Otherwise renamed.
     ai/openclaw/**           → deleted (ai/hanzo/bot/** canonical)
     Sources/OpenClaw*/       → deleted (Sources/Bot* canonical)
     Tests/OpenClawIPCTests/  → deleted (Tests/BotIPCTests canonical)
     OpenClawKit/             → deleted (BotKit canonical)
     openclaw-tools.*.ts      → deleted (bot-tools.*.ts canonical)
     openclaw-root.ts etc.    → deleted (bot-root.ts canonical)
     types.openclaw.ts        → deleted (types.bot.ts canonical)
     extensions/*/openclaw.plugin.json → renamed hanzo-bot.plugin.json
     docs/start/openclaw.md   → renamed hanzo-bot.md (+ zh-CN)
     docs/assets/openclaw-*.png, whatsapp-openclaw*.jpg → deleted
     scripts/.../openclaw-*   → deleted (bot/hanzo-bot parallels)
     openclaw.mjs             → deleted (hanzo-bot.mjs is package.bin)

Then patched 65 broken JS/TS import strings where hanzo-bot had ended
up inside an import path ('./types.hanzo-bot.js' → './types.bot.js'
etc., since the on-disk filename uses the brand-neutral bot- prefix).

Pre-existing lint debt cleaned up to get oxlint --type-aware to 0/0:
removed the dead i18n test that referenced a path no longer existing;
defined the missing DIDConfig and WalletConfig types in types.base.ts
(they were imported but never declared); dropped unused imports;
collapsed redundant type assertion and a tautological meta.bot lookup
that the openclaw→bot rename made redundant.

The a2ui.bundle.js generated artifact is kept at its pre-rebrand
contents — it gets regenerated by 'pnpm canvas:a2ui:bundle' from
sources I cannot rebuild in this commit; the source side is clean.

Verified: 0 occurrences of openclaw (case-insensitive) in tree source
content, 0 file or directory paths with openclaw in the name; oxlint
--type-aware src test reports 0/0.
2026-05-11 17:56:29 -07:00

231 lines
7.6 KiB
Swift

import Foundation
import Observation
import UserNotifications
import WatchKit
struct WatchPromptAction: Codable, Sendable, Equatable, Identifiable {
var id: String
var label: String
var style: String?
}
struct WatchNotifyMessage: Sendable {
var id: String?
var title: String
var body: String
var sentAtMs: Int?
var promptId: String?
var sessionKey: String?
var kind: String?
var details: String?
var expiresAtMs: Int?
var risk: String?
var actions: [WatchPromptAction]
}
@MainActor @Observable final class WatchInboxStore {
private struct PersistedState: Codable {
var title: String
var body: String
var transport: String
var updatedAt: Date
var lastDeliveryKey: String?
var promptId: String?
var sessionKey: String?
var kind: String?
var details: String?
var expiresAtMs: Int?
var risk: String?
var actions: [WatchPromptAction]?
var replyStatusText: String?
var replyStatusAt: Date?
}
private static let persistedStateKey = "watch.inbox.state.v1"
private let defaults: UserDefaults
var title = "Bot"
var body = "Waiting for messages from your iPhone."
var transport = "none"
var updatedAt: Date?
var promptId: String?
var sessionKey: String?
var kind: String?
var details: String?
var expiresAtMs: Int?
var risk: String?
var actions: [WatchPromptAction] = []
var replyStatusText: String?
var replyStatusAt: Date?
var isReplySending = false
private var lastDeliveryKey: String?
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.restorePersistedState()
Task {
await self.ensureNotificationAuthorization()
}
}
func consume(message: WatchNotifyMessage, transport: String) {
let messageID = message.id?
.trimmingCharacters(in: .whitespacesAndNewlines)
let deliveryKey = self.deliveryKey(
messageID: messageID,
title: message.title,
body: message.body,
sentAtMs: message.sentAtMs)
guard deliveryKey != self.lastDeliveryKey else { return }
let normalizedTitle = message.title.isEmpty ? "Bot" : message.title
self.title = normalizedTitle
self.body = message.body
self.transport = transport
self.updatedAt = Date()
self.promptId = message.promptId
self.sessionKey = message.sessionKey
self.kind = message.kind
self.details = message.details
self.expiresAtMs = message.expiresAtMs
self.risk = message.risk
self.actions = message.actions
self.lastDeliveryKey = deliveryKey
self.replyStatusText = nil
self.replyStatusAt = nil
self.isReplySending = false
self.persistState()
Task {
await self.postLocalNotification(
identifier: deliveryKey,
title: normalizedTitle,
body: message.body,
risk: message.risk)
}
}
private func restorePersistedState() {
guard let data = self.defaults.data(forKey: Self.persistedStateKey),
let state = try? JSONDecoder().decode(PersistedState.self, from: data)
else {
return
}
self.title = state.title
self.body = state.body
self.transport = state.transport
self.updatedAt = state.updatedAt
self.lastDeliveryKey = state.lastDeliveryKey
self.promptId = state.promptId
self.sessionKey = state.sessionKey
self.kind = state.kind
self.details = state.details
self.expiresAtMs = state.expiresAtMs
self.risk = state.risk
self.actions = state.actions ?? []
self.replyStatusText = state.replyStatusText
self.replyStatusAt = state.replyStatusAt
}
private func persistState() {
guard let updatedAt = self.updatedAt else { return }
let state = PersistedState(
title: self.title,
body: self.body,
transport: self.transport,
updatedAt: updatedAt,
lastDeliveryKey: self.lastDeliveryKey,
promptId: self.promptId,
sessionKey: self.sessionKey,
kind: self.kind,
details: self.details,
expiresAtMs: self.expiresAtMs,
risk: self.risk,
actions: self.actions,
replyStatusText: self.replyStatusText,
replyStatusAt: self.replyStatusAt)
guard let data = try? JSONEncoder().encode(state) else { return }
self.defaults.set(data, forKey: Self.persistedStateKey)
}
private func deliveryKey(messageID: String?, title: String, body: String, sentAtMs: Int?) -> String {
if let messageID, messageID.isEmpty == false {
return "id:\(messageID)"
}
return "content:\(title)|\(body)|\(sentAtMs ?? 0)"
}
private func ensureNotificationAuthorization() async {
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .notDetermined:
_ = try? await center.requestAuthorization(options: [.alert, .sound])
default:
break
}
}
private func mapHapticRisk(_ risk: String?) -> WKHapticType {
switch risk?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "high":
return .failure
case "medium":
return .notification
default:
return .click
}
}
func makeReplyDraft(action: WatchPromptAction) -> WatchReplyDraft {
let prompt = self.promptId?.trimmingCharacters(in: .whitespacesAndNewlines)
return WatchReplyDraft(
replyId: UUID().uuidString,
promptId: (prompt?.isEmpty == false) ? prompt! : "unknown",
actionId: action.id,
actionLabel: action.label,
sessionKey: self.sessionKey,
note: nil,
sentAtMs: Int(Date().timeIntervalSince1970 * 1000))
}
func markReplySending(actionLabel: String) {
self.isReplySending = true
self.replyStatusText = "Sending \(actionLabel)…"
self.replyStatusAt = Date()
self.persistState()
}
func markReplyResult(_ result: WatchReplySendResult, actionLabel: String) {
self.isReplySending = false
if let errorMessage = result.errorMessage, !errorMessage.isEmpty {
self.replyStatusText = "Failed: \(errorMessage)"
} else if result.deliveredImmediately {
self.replyStatusText = "\(actionLabel): sent"
} else if result.queuedForDelivery {
self.replyStatusText = "\(actionLabel): queued"
} else {
self.replyStatusText = "\(actionLabel): sent"
}
self.replyStatusAt = Date()
self.persistState()
}
private func postLocalNotification(identifier: String, title: String, body: String, risk: String?) async {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.threadIdentifier = "bot-watch"
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.2, repeats: false))
_ = try? await UNUserNotificationCenter.current().add(request)
WKInterfaceDevice.current().play(self.mapHapticRisk(risk))
}
}