From b752b83ba56ba8cb14f9f0d701d5352e10059823 Mon Sep 17 00:00:00 2001
From: Raymond
Date: Fri, 31 Jul 2026 22:18:55 +0800
Subject: [PATCH 1/3] Build standalone macOS release
---
.github/workflows/ci.yml | 36 ++
.github/workflows/release.yml | 84 +++
CHANGELOG.md | 21 +-
Package.swift | 17 +-
README.md | 72 ++-
README_EN.md | 91 +--
Resources/Info.plist | 10 +-
Sources/AgentAwake/AppDelegate.swift | 22 +-
.../IntegrationSettingsController.swift | 94 +++
Sources/AgentAwake/SettingsView.swift | 272 +++++++++
.../AgentAwake/SettingsWindowController.swift | 36 ++
Sources/AgentAwake/StatusItemController.swift | 19 +-
Sources/AgentAwake/StatusPopoverView.swift | 8 +
Sources/AgentAwakeHookSetup/main.swift | 370 ++---------
Sources/AgentAwakeSelfTest/main.swift | 171 ++++++
.../AgentIntegrationManager.swift | 572 ++++++++++++++++++
docs/RELEASING.md | 68 +++
scripts/build-app.sh | 88 ++-
scripts/package-release.sh | 111 ++++
19 files changed, 1763 insertions(+), 399 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/release.yml
create mode 100644 Sources/AgentAwake/IntegrationSettingsController.swift
create mode 100644 Sources/AgentAwake/SettingsView.swift
create mode 100644 Sources/AgentAwake/SettingsWindowController.swift
create mode 100644 Sources/AgentAwakeSetupCore/AgentIntegrationManager.swift
create mode 100644 docs/RELEASING.md
create mode 100755 scripts/package-release.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4ecd151
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,36 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: macos-15
+ timeout-minutes: 20
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Run self-test
+ run: swift run --disable-sandbox AgentAwakeSelfTest
+
+ - name: Build Universal 2 app
+ run: ./scripts/build-app.sh release universal
+
+ - name: Verify bundle and architectures
+ run: |
+ codesign --verify --deep --strict --verbose=2 dist/AgentAwake.app
+ for executable in \
+ dist/AgentAwake.app/Contents/MacOS/AgentAwake \
+ dist/AgentAwake.app/Contents/Helpers/AgentAwakeHook \
+ dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup; do
+ lipo -archs "$executable" | grep -q arm64
+ lipo -archs "$executable" | grep -q x86_64
+ done
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8ffbe00
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,84 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: macos-15
+ timeout-minutes: 30
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Verify tag matches app version
+ run: |
+ version="$(/usr/libexec/PlistBuddy \
+ -c 'Print :CFBundleShortVersionString' \
+ Resources/Info.plist)"
+ test "${GITHUB_REF_NAME#v}" = "$version"
+
+ - name: Import Developer ID certificate
+ env:
+ DEVELOPER_ID_P12_BASE64: ${{ secrets.DEVELOPER_ID_P12_BASE64 }}
+ DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }}
+ BUILD_KEYCHAIN_PASSWORD: ${{ secrets.BUILD_KEYCHAIN_PASSWORD }}
+ run: |
+ certificate_path="$RUNNER_TEMP/developer-id.p12"
+ keychain_path="$RUNNER_TEMP/agentawake-signing.keychain-db"
+ echo "$DEVELOPER_ID_P12_BASE64" | /usr/bin/base64 --decode \
+ > "$certificate_path"
+ security create-keychain \
+ -p "$BUILD_KEYCHAIN_PASSWORD" "$keychain_path"
+ security set-keychain-settings -lut 21600 "$keychain_path"
+ security unlock-keychain \
+ -p "$BUILD_KEYCHAIN_PASSWORD" "$keychain_path"
+ security import "$certificate_path" \
+ -P "$DEVELOPER_ID_P12_PASSWORD" \
+ -A \
+ -t cert \
+ -f pkcs12 \
+ -k "$keychain_path"
+ security list-keychains -d user -s "$keychain_path"
+ security set-key-partition-list \
+ -S apple-tool:,apple: \
+ -s \
+ -k "$BUILD_KEYCHAIN_PASSWORD" \
+ "$keychain_path"
+ security find-identity -v -p codesigning "$keychain_path"
+ echo "NOTARY_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
+
+ - name: Configure Apple notarization
+ env:
+ NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
+ NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ run: |
+ xcrun notarytool store-credentials agentawake-notary \
+ --apple-id "$NOTARY_APPLE_ID" \
+ --team-id "$APPLE_TEAM_ID" \
+ --password "$NOTARY_APP_PASSWORD" \
+ --keychain "$NOTARY_KEYCHAIN"
+
+ - name: Build, sign, notarize, and package
+ env:
+ DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
+ NOTARY_KEYCHAIN_PROFILE: agentawake-notary
+ run: ./scripts/package-release.sh
+
+ - name: Publish GitHub Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "$GITHUB_REF_NAME" \
+ dist/release/AgentAwake-*.dmg \
+ dist/release/AgentAwake-*.zip \
+ dist/release/AgentAwake-*.sha256 \
+ --title "AgentAwake ${GITHUB_REF_NAME#v}" \
+ --generate-notes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5151d2e..1dfccf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,10 +13,23 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
## [Unreleased / 未发布]
-
+### Added / 新增
+
+- 可独立下载的 Universal 2 App 构建,覆盖 Apple Silicon 与 Intel Mac。
+ Universal 2 app builds for both Apple silicon and Intel Macs.
+- App 内设置窗口,可选管理登录项、Codex Hooks 与 Claude Hooks。
+ In-app settings for the login item and optional Codex/Claude Hooks.
+- 首次启动自动展示菜单栏面板;定时模式无需任何 Agent 或额外配置。
+ First launch reveals the menu bar panel; timed modes need no agent or setup.
+- Developer ID 签名、Apple 公证、DMG/ZIP 制作和 GitHub Release 自动化。
+ Developer ID signing, notarization, DMG/ZIP packaging, and release automation.
+
+### Changed / 变更
+
+- Hooks 安装从命令行前置步骤改为设置中的可选增强,且只修改用户明确选择、
+ 本机已检测到的 Agent 配置。
+ Hooks are now optional settings and only modify explicitly selected,
+ locally detected agent configurations.
## [0.3.1] - 2026-07-31
diff --git a/Package.swift b/Package.swift
index 3fd78eb..b7e72c2 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,6 +13,10 @@ let package = Package(
name: "AgentAwakeCore",
targets: ["AgentAwakeCore"]
),
+ .library(
+ name: "AgentAwakeSetupCore",
+ targets: ["AgentAwakeSetupCore"]
+ ),
.executable(
name: "AgentAwake",
targets: ["AgentAwake"]
@@ -35,18 +39,25 @@ let package = Package(
),
.executableTarget(
name: "AgentAwake",
- dependencies: ["AgentAwakeCore"]
+ dependencies: ["AgentAwakeCore", "AgentAwakeSetupCore"],
+ linkerSettings: [
+ .linkedFramework("ServiceManagement")
+ ]
),
.executableTarget(
name: "AgentAwakeHook",
dependencies: ["AgentAwakeCore"]
),
.executableTarget(
- name: "AgentAwakeHookSetup"
+ name: "AgentAwakeHookSetup",
+ dependencies: ["AgentAwakeSetupCore"]
+ ),
+ .target(
+ name: "AgentAwakeSetupCore"
),
.executableTarget(
name: "AgentAwakeSelfTest",
- dependencies: ["AgentAwakeCore"]
+ dependencies: ["AgentAwakeCore", "AgentAwakeSetupCore"]
),
.executableTarget(
name: "AgentAwakePowerProbe",
diff --git a/README.md b/README.md
index cf0ae27..e7850e4 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,16 @@
- macOS 13+ · Swift · 当前版本 0.3.1
+ macOS 13+ · Apple Silicon / Intel · 当前版本 0.4.0
+
+
+
+ 下载最新版本
## 软件介绍
-AgentAwake 是一个为 Codex 和 Claude 长任务设计的轻量 macOS 菜单栏工具。
+AgentAwake 是一个下载后即可使用的轻量 macOS 菜单栏工具。
当定时保护开启或 Agent 正在工作时,它通过带超时保护的 macOS IOKit
临时电源断言阻止空闲休眠;任务结束、倒计时结束、手动关闭或退出 App 后,
它会立即释放断言,把休眠控制权交还给系统。
@@ -35,6 +39,8 @@ AgentAwake 是一个为 Codex 和 Claude 长任务设计的轻量 macOS 菜单
保持系统和显示器唤醒。
- **Hooks 优先、日志兜底**:优先使用生命周期 Hooks 获取及时状态;未安装 Hooks
时,自动增量读取本地任务日志作为零配置兜底,不依赖可能长期常驻的进程名。
+- **下载后直接使用**:普通用户不需要 Codex、Cloud、Xcode、Swift 或终端;
+ Hooks 与登录项等增强能力都在设置中按需启用。
- **自动释放与续租**:Agent 模式使用 120 秒短租约,每 60 秒续期;最后一个
Agent 结束 5 秒后自动释放,异常失联的状态也会过期。
- **安全的临时断言**:固定时长和 Agent 模式均使用带超时的 IOKit 断言,
@@ -57,24 +63,20 @@ AgentAwake 是一个为 Codex 和 Claude 长任务设计的轻量 macOS 菜单
## 系统要求
- macOS 13 Ventura 或更高版本
-- 从源码构建时需要 Swift 工具链;安装 Xcode Command Line Tools 即可
+- 支持 Apple Silicon 与 Intel Mac
- 当前菜单栏界面为简体中文
## 使用方法
-### 1. 构建并启动
+### 1. 下载并启动
-进入仓库目录后运行:
+前往 [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-```bash
-chmod +x scripts/build-app.sh scripts/build-icon.sh
-./scripts/build-app.sh
-open dist/AgentAwake.app
-```
+- **最少操作**:下载 ZIP,解压后双击 `AgentAwake.app`;
+- **标准安装**:下载 DMG,将 App 拖入“应用程序”后双击启动。
-构建脚本会生成并临时签名 `dist/AgentAwake.app`,同时把 APP 图标、主程序和
-Hooks helper 打包进去。首次启动后,AgentAwake 只出现在菜单栏,不显示 Dock
-图标。
+首次启动后,AgentAwake 会自动展开一次菜单栏面板,之后只保留菜单栏图标,
+不显示 Dock 图标。三个定时模式此时已经可以使用,不需要继续配置。
### 2. 选择保护模式
@@ -85,15 +87,20 @@ Hooks helper 打包进去。首次启动后,AgentAwake 只出现在菜单栏
固定时长模式会立即开始倒计时。Agent 模式在没有任务时只等待,
检测到 Codex 或 Claude 工作后才接管休眠。
-### 3. 安装 Agent Hooks(推荐)
+### 3. 可选设置
-先完成 App 构建,再运行幂等安装器:
+点击菜单栏面板底部的“设置”,可以按需管理:
-```bash
-./dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup install
-```
+- 登录时启动;
+- Codex Hooks;
+- Claude Hooks。
-安装器会:
+Agent 模式默认读取本机已有的 Codex 与 Claude 活动记录,因此 Hooks 不是使用
+前置条件。设置页会标明本机检测状态,只允许为实际存在的 Agent 安装 Hooks,
+不会为未安装的软件创建配置。
+AgentAwake 不负责安装或运行大模型;相关 Agent 由用户自行配置,App 只读取
+已有的本地状态。
+用户明确点击安装后,App 才会:
- 把一次性 Hook helper 安装到
`~/Library/Application Support/AgentAwake/bin/AgentAwakeHook`;
@@ -105,14 +112,8 @@ Codex 会要求审核非托管命令 Hook。安装后在 Codex CLI 输入 `/hook
检查命令路径并信任新增的 AgentAwake Hooks;未信任前 Codex 会跳过它们。
Claude 的用户级设置会自动加载。
-不安装 Hooks 也可以使用 Agent 模式,App 会自动启用本地任务日志兜底,
-只是状态变化可能不如 Hooks 及时。
-
-移除适配器:
-
-```bash
-./dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup uninstall
-```
+不安装 Hooks 也可以使用 Agent 模式,只是状态变化可能不如 Hooks 及时。
+安装、修复和移除均在设置页完成,不需要终端。
## 如何确认没有修改系统设置
@@ -155,6 +156,15 @@ app-server、sandbox 和流式进程可能长期常驻。
## 开发与验证
+从源码构建需要 Swift 工具链;安装 Xcode Command Line Tools 即可。
+
+构建 Universal 2 App:
+
+```bash
+./scripts/build-app.sh release universal
+open dist/AgentAwake.app
+```
+
运行零依赖自检:
```bash
@@ -174,14 +184,16 @@ swift run --disable-sandbox AgentAwakeSelfTest
```text
Sources/AgentAwake/ 菜单栏界面与 App 生命周期
Sources/AgentAwakeCore/ 保护会话、Agent 检测与 IOKit 断言
+Sources/AgentAwakeSetupCore/ 可选集成的检测、安装、修复与移除
Sources/AgentAwakeHook/ 单次生命周期 Hook helper
Sources/AgentAwakeHookSetup/ Hooks 安装与卸载
Resources/ Info.plist 与 APP 图标
scripts/ App 和图标构建脚本
+docs/RELEASING.md 正式签名、公证与 GitHub Release 流程
```
## 当前边界
-AgentAwake 当前面向本机运行的 Codex 与 Claude,并以源码构建方式提供。
-它不是系统电源设置管理器,也不会替代 macOS 原有的睡眠策略;它只在所选窗口内
-临时延后空闲休眠。
+AgentAwake 的定时模式不依赖任何 Agent;Agent 感知模式当前支持本机运行的
+Codex 与 Claude。它不是系统电源设置管理器,也不会替代 macOS 原有的睡眠
+策略;它只在所选窗口内临时延后空闲休眠。
diff --git a/README_EN.md b/README_EN.md
index 85a16d8..ef4cad0 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -14,13 +14,17 @@
- macOS 13+ · Swift · Current version 0.3.1
+ macOS 13+ · Apple Silicon / Intel · Current version 0.4.0
+
+
+
+ Download the latest release
## About
-AgentAwake is a lightweight macOS menu bar utility for long-running Codex and
-Claude tasks. While a timed mode is active or an agent is working, it prevents
+AgentAwake is a lightweight macOS menu bar utility that is ready to use after
+download. While a timed mode is active or an agent is working, it prevents
idle sleep with timeout-protected macOS IOKit power assertions. When the task
finishes, the timer expires, the mode is turned off, or the app quits,
AgentAwake immediately releases those assertions and returns control to macOS.
@@ -37,6 +41,8 @@ or keeps a persistent `caffeinate` or shell process running.
- **Hooks first, local logs as fallback** — Uses lifecycle Hooks for timely
state changes. Without Hooks, it incrementally reads local task logs for a
zero-configuration fallback instead of trusting long-lived process names.
+- **Download and run** — Regular users need no Codex, Cloud, Xcode, Swift, or
+ terminal. Hooks and login-item behavior are optional settings.
- **Automatic release and renewal** — Agent mode uses a 120-second lease,
renews it every 60 seconds, and releases it five seconds after the last agent
finishes. Stale activity also expires automatically.
@@ -62,24 +68,23 @@ or keeps a persistent `caffeinate` or shell process running.
## Requirements
- macOS 13 Ventura or later
-- A Swift toolchain for source builds; Xcode Command Line Tools are sufficient
+- Apple silicon and Intel Macs are supported
- The current menu bar interface is in Simplified Chinese
## Usage
-### 1. Build and launch
+### 1. Download and launch
-From the repository root:
+Open [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-```bash
-chmod +x scripts/build-app.sh scripts/build-icon.sh
-./scripts/build-app.sh
-open dist/AgentAwake.app
-```
+- **Fewest steps:** download the ZIP, extract it, and double-click
+ `AgentAwake.app`.
+- **Standard installation:** download the DMG, drag the app to Applications,
+ and double-click it.
-The build script creates an ad-hoc signed `dist/AgentAwake.app` containing the
-app icon, main executable, and Hook helpers. Once launched, AgentAwake appears
-only in the menu bar and does not show a Dock icon.
+On its first launch, AgentAwake opens its menu bar panel once. It then remains
+in the menu bar without a Dock icon. All three timed modes are immediately
+available and require no further setup.
### 2. Choose a protection mode
@@ -91,36 +96,38 @@ only in the menu bar and does not show a Dock icon.
Timed modes start their countdown immediately. Agent mode waits without holding
an assertion, then takes over only when it detects active Codex or Claude work.
-### 3. Install Agent Hooks (recommended)
+### 3. Optional settings
-Build the app first, then run the idempotent installer:
+Click Settings at the bottom of the menu bar panel to manage:
-```bash
-./dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup install
-```
+- launch at login;
+- Codex Hooks;
+- Claude Hooks.
-The installer:
+Agent mode reads existing local Codex and Claude activity records by default,
+so Hooks are not a prerequisite. Settings reports local detection state and
+only enables Hook installation for agents that exist on the Mac. It never
+creates configuration for absent software. Only after the user explicitly
+selects Install does the app:
-- places the one-shot helper at
+- place the one-shot helper at
`~/Library/Application Support/AgentAwake/bin/AgentAwakeHook`;
-- safely merges entries into `~/.codex/hooks.json` and
+- safely merge entries into `~/.codex/hooks.json` and
`~/.claude/settings.json`;
-- creates an `.agentawake-backup` before it first changes an existing config;
-- updates its own entries on repeated runs instead of appending duplicates.
+- create an `.agentawake-backup` before it first changes an existing config;
+- update its own entries on repeated runs instead of appending duplicates.
+
+AgentAwake does not install or run a model. Users configure their own agent
+software, and the app only reads existing local state.
Codex asks you to review unmanaged command Hooks. After installation, enter
`/hooks` in the Codex CLI, inspect the command path, and trust the new
AgentAwake Hooks. Codex skips them until they are trusted. Claude loads its
user-level settings automatically.
-Agent mode still works without Hooks by using the local task-log fallback,
-although state transitions may be less immediate.
-
-To remove the adapters:
-
-```bash
-./dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup uninstall
-```
+Agent mode still works without Hooks, although state transitions may be less
+immediate. Installation, repair, and removal all happen in Settings without a
+terminal.
## Verify that system settings remain unchanged
@@ -167,6 +174,16 @@ automatically.
## Development and verification
+Source builds require a Swift toolchain; Xcode Command Line Tools are
+sufficient.
+
+Build the Universal 2 app:
+
+```bash
+./scripts/build-app.sh release universal
+open dist/AgentAwake.app
+```
+
Run the dependency-free self-test:
```bash
@@ -186,15 +203,17 @@ Project layout:
```text
Sources/AgentAwake/ Menu bar UI and app lifecycle
Sources/AgentAwakeCore/ Protection state, agent detection, IOKit assertions
+Sources/AgentAwakeSetupCore/ Optional integration detection and management
Sources/AgentAwakeHook/ One-shot lifecycle Hook helper
Sources/AgentAwakeHookSetup/ Hook installer and uninstaller
Resources/ Info.plist and app icon
scripts/ App and icon build scripts
+docs/RELEASING.md Signing, notarization, and GitHub Release workflow
```
## Current scope
-AgentAwake currently targets locally running Codex and Claude tasks and is
-distributed as a source build. It is not a system power-settings manager and
-does not replace macOS sleep policy; it only postpones idle sleep during the
-selected protection window.
+AgentAwake timed modes do not require an agent. Agent-aware mode currently
+supports locally running Codex and Claude tasks. It is not a system
+power-settings manager and does not replace macOS sleep policy; it only
+postpones idle sleep during the selected protection window.
diff --git a/Resources/Info.plist b/Resources/Info.plist
index f2fa80d..6659603 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -19,13 +19,19 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.3.1
+ 0.4.0
CFBundleVersion
- 4
+ 5
+ LSApplicationCategoryType
+ public.app-category.utilities
LSMinimumSystemVersion
13.0
+ LSMultipleInstancesProhibited
+
LSUIElement
+ NSHumanReadableCopyright
+ Copyright © 2026 Raymond. All rights reserved.
NSHighResolutionCapable
diff --git a/Sources/AgentAwake/AppDelegate.swift b/Sources/AgentAwake/AppDelegate.swift
index bbce45b..4662903 100644
--- a/Sources/AgentAwake/AppDelegate.swift
+++ b/Sources/AgentAwake/AppDelegate.swift
@@ -2,14 +2,34 @@ import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
private var appController: AppController?
+ private var integrationSettingsController: IntegrationSettingsController?
+ private var settingsWindowController: SettingsWindowController?
private var statusItemController: StatusItemController?
func applicationDidFinishLaunching(_ notification: Notification) {
NSApplication.shared.setActivationPolicy(.accessory)
let appController = AppController()
+ let integrationSettingsController = IntegrationSettingsController()
+ let settingsWindowController = SettingsWindowController(
+ settingsController: integrationSettingsController
+ )
self.appController = appController
- statusItemController = StatusItemController(appController: appController)
+ self.integrationSettingsController = integrationSettingsController
+ self.settingsWindowController = settingsWindowController
+ let statusItemController = StatusItemController(
+ appController: appController,
+ onOpenSettings: settingsWindowController.present
+ )
+ self.statusItemController = statusItemController
+
+ let defaults = UserDefaults.standard
+ if !defaults.bool(forKey: "hasPresentedInitialPopover") {
+ defaults.set(true, forKey: "hasPresentedInitialPopover")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
+ statusItemController.presentPopover()
+ }
+ }
}
func applicationWillTerminate(_ notification: Notification) {
diff --git a/Sources/AgentAwake/IntegrationSettingsController.swift b/Sources/AgentAwake/IntegrationSettingsController.swift
new file mode 100644
index 0000000..c82c6d4
--- /dev/null
+++ b/Sources/AgentAwake/IntegrationSettingsController.swift
@@ -0,0 +1,94 @@
+import AgentAwakeSetupCore
+import Foundation
+import ServiceManagement
+
+@MainActor
+final class IntegrationSettingsController: ObservableObject {
+ @Published private(set) var integrations: [AgentIntegrationSnapshot] = []
+ @Published private(set) var workingProvider: AgentIntegrationProvider?
+ @Published private(set) var feedbackText: String?
+ @Published private(set) var launchAtLoginEnabled = false
+ @Published private(set) var launchAtLoginNeedsApproval = false
+ @Published private(set) var isChangingLaunchAtLogin = false
+
+ private let manager: AgentIntegrationManager
+ private let loginService: SMAppService
+
+ init(
+ manager: AgentIntegrationManager? = nil,
+ loginService: SMAppService = .mainApp
+ ) {
+ let helperURL = Bundle.main.bundleURL
+ .appendingPathComponent("Contents", isDirectory: true)
+ .appendingPathComponent("Helpers", isDirectory: true)
+ .appendingPathComponent("AgentAwakeHook")
+ self.manager = manager ?? AgentIntegrationManager(
+ bundledHelperURL: helperURL
+ )
+ self.loginService = loginService
+ refresh()
+ }
+
+ func refresh() {
+ integrations = manager.inspectAll()
+ launchAtLoginEnabled = loginService.status == .enabled
+ launchAtLoginNeedsApproval = loginService.status == .requiresApproval
+ }
+
+ func install(_ provider: AgentIntegrationProvider) {
+ guard workingProvider == nil else {
+ return
+ }
+
+ workingProvider = provider
+ feedbackText = nil
+ do {
+ try manager.install(provider)
+ feedbackText = "\(provider.displayName) Hooks 已安装。"
+ } catch {
+ feedbackText = error.localizedDescription
+ }
+ workingProvider = nil
+ refresh()
+ }
+
+ func uninstall(_ provider: AgentIntegrationProvider) {
+ guard workingProvider == nil else {
+ return
+ }
+
+ workingProvider = provider
+ feedbackText = nil
+ do {
+ try manager.uninstall(provider)
+ feedbackText = "\(provider.displayName) Hooks 已移除。"
+ } catch {
+ feedbackText = error.localizedDescription
+ }
+ workingProvider = nil
+ refresh()
+ }
+
+ func setLaunchAtLogin(_ enabled: Bool) {
+ guard !isChangingLaunchAtLogin else {
+ return
+ }
+
+ isChangingLaunchAtLogin = true
+ feedbackText = nil
+
+ Task { @MainActor in
+ do {
+ if enabled {
+ try loginService.register()
+ } else {
+ try await loginService.unregister()
+ }
+ } catch {
+ feedbackText = "无法更新登录项:\(error.localizedDescription)"
+ }
+ isChangingLaunchAtLogin = false
+ refresh()
+ }
+ }
+}
diff --git a/Sources/AgentAwake/SettingsView.swift b/Sources/AgentAwake/SettingsView.swift
new file mode 100644
index 0000000..839aae5
--- /dev/null
+++ b/Sources/AgentAwake/SettingsView.swift
@@ -0,0 +1,272 @@
+import AgentAwakeSetupCore
+import SwiftUI
+
+struct SettingsView: View {
+ @ObservedObject var controller: IntegrationSettingsController
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 22) {
+ header
+ generalSection
+ integrationSection
+
+ if let feedbackText = controller.feedbackText {
+ Label(feedbackText, systemImage: "info.circle")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ .textSelection(.enabled)
+ }
+
+ footer
+ }
+ .padding(24)
+ }
+ .frame(width: 540, height: 520)
+ .onAppear {
+ controller.refresh()
+ }
+ }
+
+ private var header: some View {
+ VStack(alignment: .leading, spacing: 5) {
+ Text("设置")
+ .font(.system(size: 22, weight: .semibold))
+
+ Text(
+ "AgentAwake 下载后即可使用;" +
+ "下面的项目都不是启动前置条件。"
+ )
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ private var generalSection: some View {
+ settingsCard(title: "通用", systemImage: "gearshape") {
+ VStack(alignment: .leading, spacing: 12) {
+ Toggle(
+ "登录时启动 AgentAwake",
+ isOn: Binding(
+ get: { controller.launchAtLoginEnabled },
+ set: controller.setLaunchAtLogin
+ )
+ )
+ .disabled(controller.isChangingLaunchAtLogin)
+
+ if controller.launchAtLoginNeedsApproval {
+ Text(
+ "macOS 正在等待你在“系统设置 → 登录项”中" +
+ "允许此项目。"
+ )
+ .font(.system(size: 11))
+ .foregroundStyle(.orange)
+ } else {
+ Text(
+ "App 每次启动仍会回到“未开启”," +
+ "不会自动接管休眠。"
+ )
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+
+ private var integrationSection: some View {
+ settingsCard(title: "Agent 集成(可选)", systemImage: "puzzlepiece") {
+ VStack(alignment: .leading, spacing: 14) {
+ Text(
+ "AgentAwake 不安装或运行大模型,只读取用户已经配置好的" +
+ "本机状态。安装 Hooks 只用于让开始和结束更及时," +
+ "不安装也能使用。"
+ )
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ .fixedSize(horizontal: false, vertical: true)
+
+ ForEach(controller.integrations) { integration in
+ integrationRow(integration)
+
+ if integration.id != controller.integrations.last?.id {
+ Divider()
+ }
+ }
+ }
+ }
+ }
+
+ private func integrationRow(
+ _ integration: AgentIntegrationSnapshot
+ ) -> some View {
+ HStack(alignment: .center, spacing: 12) {
+ Image(systemName: providerSymbol(integration.provider))
+ .font(.system(size: 16, weight: .medium))
+ .frame(width: 26, height: 26)
+ .background(
+ Color.primary.opacity(0.06),
+ in: RoundedRectangle(cornerRadius: 7)
+ )
+
+ VStack(alignment: .leading, spacing: 3) {
+ HStack(spacing: 7) {
+ Text(integration.provider.displayName)
+ .font(.system(size: 13, weight: .medium))
+
+ statusBadge(integration.state)
+ }
+
+ Text(integrationDescription(integration))
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ }
+
+ Spacer(minLength: 12)
+ integrationActions(integration)
+ }
+ }
+
+ @ViewBuilder
+ private func integrationActions(
+ _ integration: AgentIntegrationSnapshot
+ ) -> some View {
+ let isWorking = controller.workingProvider == integration.provider
+
+ switch integration.state {
+ case .notDetected:
+ Button("未检测到") {}
+ .disabled(true)
+
+ case .available:
+ Button(isWorking ? "安装中…" : "安装 Hooks") {
+ controller.install(integration.provider)
+ }
+ .disabled(controller.workingProvider != nil)
+
+ case .installed:
+ Button(isWorking ? "移除中…" : "移除") {
+ controller.uninstall(integration.provider)
+ }
+ .disabled(controller.workingProvider != nil)
+
+ case .needsRepair:
+ Button(isWorking ? "修复中…" : "修复") {
+ controller.install(integration.provider)
+ }
+ .disabled(controller.workingProvider != nil)
+
+ case .invalidConfiguration:
+ Button("需手动检查") {}
+ .disabled(true)
+ }
+ }
+
+ private func statusBadge(
+ _ state: AgentIntegrationState
+ ) -> some View {
+ Text(statusText(state))
+ .font(.system(size: 9, weight: .medium))
+ .foregroundStyle(statusColor(state))
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(
+ statusColor(state).opacity(0.1),
+ in: Capsule()
+ )
+ }
+
+ private func settingsCard(
+ title: String,
+ systemImage: String,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ VStack(alignment: .leading, spacing: 14) {
+ Label(title, systemImage: systemImage)
+ .font(.system(size: 13, weight: .semibold))
+
+ content()
+ }
+ .padding(16)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ Color.primary.opacity(0.045),
+ in: RoundedRectangle(cornerRadius: 12, style: .continuous)
+ )
+ }
+
+ private var footer: some View {
+ HStack {
+ Label("完全本地运行 · 无账号 · 无云服务", systemImage: "lock")
+ Spacer()
+ Text(versionText)
+ }
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ }
+
+ private var versionText: String {
+ let version = Bundle.main.object(
+ forInfoDictionaryKey: "CFBundleShortVersionString"
+ ) as? String ?? "开发版"
+ return "AgentAwake \(version)"
+ }
+
+ private func providerSymbol(
+ _ provider: AgentIntegrationProvider
+ ) -> String {
+ switch provider {
+ case .codex:
+ return "terminal"
+ case .claude:
+ return "text.bubble"
+ }
+ }
+
+ private func statusText(_ state: AgentIntegrationState) -> String {
+ switch state {
+ case .notDetected:
+ return "未检测到"
+ case .available:
+ return "可选"
+ case .installed:
+ return "已安装"
+ case .needsRepair:
+ return "需更新"
+ case .invalidConfiguration:
+ return "配置异常"
+ }
+ }
+
+ private func statusColor(_ state: AgentIntegrationState) -> Color {
+ switch state {
+ case .installed:
+ return .green
+ case .needsRepair, .invalidConfiguration:
+ return .orange
+ case .notDetected, .available:
+ return .secondary
+ }
+ }
+
+ private func integrationDescription(
+ _ integration: AgentIntegrationSnapshot
+ ) -> String {
+ switch integration.state {
+ case .notDetected:
+ return "不会创建 \(integration.provider.displayName) 配置。"
+ case .available where integration.hasLocalActivityData:
+ return "已发现本地活动记录;当前可直接使用日志检测。"
+ case .available:
+ return "已检测到应用;Hooks 尚未安装。"
+ case .installed where integration.provider == .codex:
+ return "Hook 已配置;Codex 首次使用时仍需在 /hooks 中确认信任。"
+ case .installed:
+ return "Hook 已配置,状态变化会更及时。"
+ case .needsRepair:
+ return "已安装的 helper 与当前 App 版本不一致。"
+ case let .invalidConfiguration(message):
+ return message
+ }
+ }
+}
diff --git a/Sources/AgentAwake/SettingsWindowController.swift b/Sources/AgentAwake/SettingsWindowController.swift
new file mode 100644
index 0000000..251494d
--- /dev/null
+++ b/Sources/AgentAwake/SettingsWindowController.swift
@@ -0,0 +1,36 @@
+import AppKit
+import SwiftUI
+
+@MainActor
+final class SettingsWindowController: NSWindowController, NSWindowDelegate {
+ init(settingsController: IntegrationSettingsController) {
+ let hostingController = NSHostingController(
+ rootView: SettingsView(controller: settingsController)
+ )
+ let window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 540, height: 520),
+ styleMask: [.titled, .closable, .miniaturizable],
+ backing: .buffered,
+ defer: false
+ )
+ window.title = "AgentAwake 设置"
+ window.contentViewController = hostingController
+ window.isReleasedWhenClosed = false
+ window.center()
+
+ super.init(window: window)
+ window.delegate = self
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func present() {
+ showWindow(nil)
+ window?.center()
+ window?.makeKeyAndOrderFront(nil)
+ NSApplication.shared.activate(ignoringOtherApps: true)
+ }
+}
diff --git a/Sources/AgentAwake/StatusItemController.swift b/Sources/AgentAwake/StatusItemController.swift
index 0565f81..649a664 100644
--- a/Sources/AgentAwake/StatusItemController.swift
+++ b/Sources/AgentAwake/StatusItemController.swift
@@ -6,9 +6,14 @@ final class StatusItemController: NSObject {
private let statusItem: NSStatusItem
private let popover: NSPopover
private let appController: AppController
+ private let onOpenSettings: () -> Void
- init(appController: AppController) {
+ init(
+ appController: AppController,
+ onOpenSettings: @escaping () -> Void
+ ) {
self.appController = appController
+ self.onOpenSettings = onOpenSettings
self.statusItem = NSStatusBar.system.statusItem(
withLength: 31
)
@@ -41,7 +46,13 @@ final class StatusItemController: NSObject {
popover.animates = true
popover.contentSize = NSSize(width: 340, height: 290)
popover.contentViewController = NSHostingController(
- rootView: StatusPopoverView(appController: appController)
+ rootView: StatusPopoverView(
+ appController: appController,
+ onOpenSettings: { [weak self] in
+ self?.popover.performClose(nil)
+ self?.onOpenSettings()
+ }
+ )
)
}
@@ -78,10 +89,10 @@ final class StatusItemController: NSObject {
return
}
- showPopover()
+ presentPopover()
}
- private func showPopover() {
+ func presentPopover() {
guard let button = statusItem.button else {
return
}
diff --git a/Sources/AgentAwake/StatusPopoverView.swift b/Sources/AgentAwake/StatusPopoverView.swift
index bcb95a9..e87e49a 100644
--- a/Sources/AgentAwake/StatusPopoverView.swift
+++ b/Sources/AgentAwake/StatusPopoverView.swift
@@ -4,6 +4,7 @@ import SwiftUI
struct StatusPopoverView: View {
@ObservedObject var appController: AppController
+ let onOpenSettings: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
@@ -118,6 +119,13 @@ struct StatusPopoverView: View {
Spacer()
+ Button("设置") {
+ onOpenSettings()
+ }
+ .buttonStyle(.plain)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+
Button("退出") {
NSApplication.shared.terminate(nil)
}
diff --git a/Sources/AgentAwakeHookSetup/main.swift b/Sources/AgentAwakeHookSetup/main.swift
index 92bbefb..8bb181d 100644
--- a/Sources/AgentAwakeHookSetup/main.swift
+++ b/Sources/AgentAwakeHookSetup/main.swift
@@ -1,50 +1,27 @@
+import AgentAwakeSetupCore
import Darwin
import Foundation
-private let adapterID = "com.raymond.agentawake"
-private let codexEvents = [
- "UserPromptSubmit",
- "PreToolUse",
- "PermissionRequest",
- "PostToolUse",
- "Stop",
- "SessionEnd"
-]
-private let claudeEvents = [
- "UserPromptSubmit",
- "PreToolUse",
- "PermissionRequest",
- "PostToolUse",
- "PostToolUseFailure",
- "Stop",
- "StopFailure",
- "SessionEnd"
-]
-
private enum SetupAction: String {
case install
case uninstall
+ case status
}
-private enum SetupError: LocalizedError {
+private enum SetupCommandError: LocalizedError {
case usage
- case helperMissing(String)
- case invalidJSONObject(String)
- case invalidHooks(String)
+ case invalidProvider(String)
var errorDescription: String? {
switch self {
case .usage:
return """
- 用法:AgentAwakeHookSetup install|uninstall \
- [--home 路径] [--helper AgentAwakeHook路径]
+ 用法:AgentAwakeHookSetup install|uninstall|status \
+ [--provider codex|claude] [--home 路径] \
+ [--helper AgentAwakeHook路径]
"""
- case let .helperMissing(path):
- return "找不到 AgentAwakeHook:\(path)"
- case let .invalidJSONObject(path):
- return "配置不是有效的 JSON 对象,未修改:\(path)"
- case let .invalidHooks(path):
- return "配置中的 hooks 结构无法安全合并,未修改:\(path)"
+ case let .invalidProvider(value):
+ return "不支持的 Agent:\(value)"
}
}
}
@@ -55,264 +32,29 @@ private func option(named name: String) -> String? {
else {
return nil
}
-
return CommandLine.arguments[index + 1]
}
-private func loadJSONObject(
- at url: URL,
- defaultValue: [String: Any]
-) throws -> [String: Any] {
- guard FileManager.default.fileExists(atPath: url.path) else {
- return defaultValue
- }
-
- let data = try Data(contentsOf: url)
- guard let object = try JSONSerialization.jsonObject(with: data)
- as? [String: Any]
- else {
- throw SetupError.invalidJSONObject(url.path)
- }
-
- return object
-}
-
-private func hookGroups(
- from value: Any?,
- configPath: String
-) throws -> [[String: Any]] {
- guard let value else {
- return []
- }
-
- guard let groups = value as? [[String: Any]] else {
- throw SetupError.invalidHooks(configPath)
- }
- return groups
-}
-
-private func isAgentAwakeHandler(_ handler: [String: Any]) -> Bool {
- if let command = handler["command"] as? String,
- command.contains("--adapter-id \(adapterID)")
- {
- return true
- }
-
- if let arguments = handler["args"] as? [String],
- let markerIndex = arguments.firstIndex(of: "--adapter-id"),
- arguments.indices.contains(markerIndex + 1),
- arguments[markerIndex + 1] == adapterID
- {
- return true
- }
-
- return false
-}
-
-private func removingAgentAwakeHandlers(
- from groups: [[String: Any]]
-) throws -> [[String: Any]] {
- try groups.compactMap { originalGroup in
- var group = originalGroup
- guard let handlersValue = group["hooks"] else {
- return group
- }
- guard let handlers = handlersValue as? [[String: Any]] else {
- throw SetupError.invalidHooks("hooks[].hooks")
- }
-
- let retained = handlers.filter { !isAgentAwakeHandler($0) }
- guard !retained.isEmpty else {
- return nil
- }
-
- group["hooks"] = retained
- return group
- }
-}
-
-private func singleQuotedShellArgument(_ value: String) -> String {
- "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
-}
-
-private func codexHandler(helperPath: String) -> [String: Any] {
- let command = [
- singleQuotedShellArgument(helperPath),
- "--provider codex",
- "--adapter-id \(adapterID)"
- ].joined(separator: " ")
-
- return [
- "type": "command",
- "command": command,
- "timeout": 3
- ]
-}
-
-private func claudeHandler(helperPath: String) -> [String: Any] {
- [
- "type": "command",
- "command": helperPath,
- "args": [
- "--provider",
- "claude",
- "--adapter-id",
- adapterID
- ],
- "timeout": 3
- ]
-}
-
-private func updateHooks(
- in root: inout [String: Any],
- events: [String],
- handler: [String: Any]?,
- configPath: String
-) throws {
- let existingHooks = root["hooks"] as? [String: Any]
- if root["hooks"] != nil, existingHooks == nil {
- throw SetupError.invalidHooks(configPath)
- }
-
- var hooks = existingHooks ?? [:]
-
- for event in events {
- let groups = try hookGroups(
- from: hooks[event],
- configPath: configPath
- )
- var updated = try removingAgentAwakeHandlers(from: groups)
-
- if let handler {
- updated.append(["hooks": [handler]])
- }
-
- if updated.isEmpty {
- hooks.removeValue(forKey: event)
- } else {
- hooks[event] = updated
- }
- }
-
- root["hooks"] = hooks
-}
-
-private func createBackupIfNeeded(at url: URL) throws {
- guard FileManager.default.fileExists(atPath: url.path) else {
- return
+private func stateDescription(_ state: AgentIntegrationState) -> String {
+ switch state {
+ case .notDetected:
+ return "未检测到"
+ case .available:
+ return "可选,尚未安装 Hooks"
+ case .installed:
+ return "Hooks 已安装"
+ case .needsRepair:
+ return "需要修复或更新"
+ case let .invalidConfiguration(message):
+ return "配置异常:\(message)"
}
-
- let backupURL = URL(fileURLWithPath: url.path + ".agentawake-backup")
- guard !FileManager.default.fileExists(atPath: backupURL.path) else {
- return
- }
-
- try FileManager.default.copyItem(at: url, to: backupURL)
-}
-
-private func writeJSONObject(
- _ object: [String: Any],
- to url: URL
-) throws {
- try FileManager.default.createDirectory(
- at: url.deletingLastPathComponent(),
- withIntermediateDirectories: true
- )
- try createBackupIfNeeded(at: url)
-
- var data = try JSONSerialization.data(
- withJSONObject: object,
- options: [.prettyPrinted, .sortedKeys]
- )
- data.append(0x0A)
- try data.write(to: url, options: .atomic)
- try? FileManager.default.setAttributes(
- [.posixPermissions: 0o600],
- ofItemAtPath: url.path
- )
-}
-
-private func installStableHelper(
- sourceURL: URL,
- homeDirectory: URL
-) throws -> URL {
- guard FileManager.default.isExecutableFile(atPath: sourceURL.path) else {
- throw SetupError.helperMissing(sourceURL.path)
- }
-
- let destinationDirectory = homeDirectory
- .appendingPathComponent("Library", isDirectory: true)
- .appendingPathComponent("Application Support", isDirectory: true)
- .appendingPathComponent("AgentAwake", isDirectory: true)
- .appendingPathComponent("bin", isDirectory: true)
- let destinationURL = destinationDirectory
- .appendingPathComponent("AgentAwakeHook")
-
- try FileManager.default.createDirectory(
- at: destinationDirectory,
- withIntermediateDirectories: true
- )
-
- if sourceURL.standardizedFileURL != destinationURL.standardizedFileURL {
- let data = try Data(contentsOf: sourceURL)
- try data.write(to: destinationURL, options: .atomic)
- }
-
- try FileManager.default.setAttributes(
- [.posixPermissions: 0o755],
- ofItemAtPath: destinationURL.path
- )
- return destinationURL
-}
-
-private func configureCodex(
- action: SetupAction,
- helperPath: String?,
- homeDirectory: URL
-) throws {
- let url = homeDirectory
- .appendingPathComponent(".codex", isDirectory: true)
- .appendingPathComponent("hooks.json")
- var root = try loadJSONObject(
- at: url,
- defaultValue: [
- "description": "Personal lifecycle hooks."
- ]
- )
-
- try updateHooks(
- in: &root,
- events: codexEvents,
- handler: helperPath.map(codexHandler),
- configPath: url.path
- )
- try writeJSONObject(root, to: url)
-}
-
-private func configureClaude(
- action: SetupAction,
- helperPath: String?,
- homeDirectory: URL
-) throws {
- let url = homeDirectory
- .appendingPathComponent(".claude", isDirectory: true)
- .appendingPathComponent("settings.json")
- var root = try loadJSONObject(at: url, defaultValue: [:])
-
- try updateHooks(
- in: &root,
- events: claudeEvents,
- handler: helperPath.map(claudeHandler),
- configPath: url.path
- )
- try writeJSONObject(root, to: url)
}
do {
guard CommandLine.arguments.count >= 2,
let action = SetupAction(rawValue: CommandLine.arguments[1])
else {
- throw SetupError.usage
+ throw SetupCommandError.usage
}
let homeDirectory = option(named: "--home").map {
@@ -322,40 +64,60 @@ do {
let executableURL = URL(
fileURLWithPath: CommandLine.arguments[0]
).standardizedFileURL
- let sourceHelperURL = option(named: "--helper").map {
+ let helperURL = option(named: "--helper").map {
URL(fileURLWithPath: $0)
} ?? executableURL
.deletingLastPathComponent()
.appendingPathComponent("AgentAwakeHook")
- let installedHelperURL: URL?
- switch action {
- case .install:
- installedHelperURL = try installStableHelper(
- sourceURL: sourceHelperURL,
- homeDirectory: homeDirectory
- )
- case .uninstall:
- installedHelperURL = nil
- }
-
- try configureCodex(
- action: action,
- helperPath: installedHelperURL?.path,
- homeDirectory: homeDirectory
- )
- try configureClaude(
- action: action,
- helperPath: installedHelperURL?.path,
- homeDirectory: homeDirectory
+ let manager = AgentIntegrationManager(
+ homeDirectory: homeDirectory,
+ bundledHelperURL: helperURL
)
+ let providers: [AgentIntegrationProvider]
+ if let providerName = option(named: "--provider") {
+ guard let provider = AgentIntegrationProvider(
+ rawValue: providerName.lowercased()
+ ) else {
+ throw SetupCommandError.invalidProvider(providerName)
+ }
+ providers = [provider]
+ } else {
+ providers = manager.detectedProviders()
+ }
+
switch action {
case .install:
- print("AgentAwake Hooks 已写入 Codex 与 Claude 配置。")
- print("Codex 还需在 /hooks 中审核并信任新增命令。")
+ guard !providers.isEmpty else {
+ print("未检测到 Codex 或 Claude;未创建任何配置。")
+ exit(EXIT_SUCCESS)
+ }
+ for provider in providers {
+ try manager.install(provider)
+ print("\(provider.displayName) Hooks 已安装。")
+ }
+ if providers.contains(.codex) {
+ print("Codex 还需在 /hooks 中审核并信任新增命令。")
+ }
+
case .uninstall:
- print("AgentAwake Hooks 已从 Codex 与 Claude 配置移除。")
+ for provider in providers {
+ try manager.uninstall(provider)
+ print("\(provider.displayName) Hooks 已移除。")
+ }
+
+ case .status:
+ let snapshots = manager.inspectAll().filter { snapshot in
+ option(named: "--provider") == nil
+ || providers.contains(snapshot.provider)
+ }
+ for snapshot in snapshots {
+ print(
+ "\(snapshot.provider.displayName):" +
+ stateDescription(snapshot.state)
+ )
+ }
}
} catch {
fputs("AgentAwake Hook Setup: \(error.localizedDescription)\n", stderr)
diff --git a/Sources/AgentAwakeSelfTest/main.swift b/Sources/AgentAwakeSelfTest/main.swift
index fd2ac1d..faa549b 100644
--- a/Sources/AgentAwakeSelfTest/main.swift
+++ b/Sources/AgentAwakeSelfTest/main.swift
@@ -1,4 +1,5 @@
import AgentAwakeCore
+import AgentAwakeSetupCore
import Darwin
import Foundation
@@ -388,10 +389,180 @@ private func testAgentActivityParsing() {
)
}
+private func testSelectiveIntegrationSetup() {
+ let fileManager = FileManager.default
+ let root = fileManager.temporaryDirectory
+ .appendingPathComponent(
+ "AgentAwakeSetupTest-\(UUID().uuidString)",
+ isDirectory: true
+ )
+ defer {
+ try? fileManager.removeItem(at: root)
+ }
+
+ let codexDirectory = root.appendingPathComponent(
+ ".codex",
+ isDirectory: true
+ )
+ let sessionsDirectory = codexDirectory.appendingPathComponent(
+ "sessions",
+ isDirectory: true
+ )
+ try? fileManager.createDirectory(
+ at: sessionsDirectory,
+ withIntermediateDirectories: true
+ )
+
+ let configURL = codexDirectory.appendingPathComponent("hooks.json")
+ let originalConfig: [String: Any] = [
+ "description": "Existing user hooks",
+ "hooks": [
+ "UserPromptSubmit": [
+ [
+ "hooks": [
+ [
+ "type": "command",
+ "command": "/usr/bin/true",
+ "timeout": 2
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ if let data = try? JSONSerialization.data(
+ withJSONObject: originalConfig,
+ options: [.prettyPrinted, .sortedKeys]
+ ) {
+ try? data.write(to: configURL)
+ }
+
+ let helperURL = root.appendingPathComponent("AgentAwakeHook")
+ try? Data("agentawake-test-helper".utf8).write(to: helperURL)
+ try? fileManager.setAttributes(
+ [.posixPermissions: 0o755],
+ ofItemAtPath: helperURL.path
+ )
+
+ let manager = AgentIntegrationManager(
+ homeDirectory: root,
+ bundledHelperURL: helperURL
+ )
+ let initial = manager.inspect(.codex)
+ expect(initial.state == .available, "检测到 Codex 后 Hooks 应保持可选")
+ expect(initial.hasLocalActivityData, "应识别已有 Codex 活动目录")
+
+ do {
+ try manager.install(.codex)
+ } catch {
+ failures.append("Codex Hooks 安装失败:\(error.localizedDescription)")
+ }
+
+ expect(
+ manager.inspect(.codex).state == .installed,
+ "安装后 Codex Hooks 状态应为已安装"
+ )
+ expect(
+ !fileManager.fileExists(
+ atPath: root
+ .appendingPathComponent(".claude/settings.json")
+ .path
+ ),
+ "安装 Codex 集成时不应创建 Claude 配置"
+ )
+ expect(
+ fileManager.fileExists(
+ atPath: configURL.path + ".agentawake-backup"
+ ),
+ "首次修改已有配置前应创建备份"
+ )
+
+ let installedConfig = (try? String(contentsOf: configURL)) ?? ""
+ expect(
+ installedConfig.contains("usr") && installedConfig.contains("true"),
+ "安装 Hooks 时必须保留用户原有命令"
+ )
+ expect(
+ installedConfig.contains(AgentIntegrationManager.adapterID),
+ "安装后配置应包含 AgentAwake 标识"
+ )
+
+ do {
+ try manager.install(.codex)
+ let repeatedConfig = (try? String(contentsOf: configURL)) ?? ""
+ let adapterCount = repeatedConfig.components(
+ separatedBy: AgentIntegrationManager.adapterID
+ ).count - 1
+ expect(adapterCount == 6, "重复安装不应追加重复的 Codex Hooks")
+ try manager.uninstall(.codex)
+ } catch {
+ failures.append("Codex Hooks 更新或移除失败:\(error.localizedDescription)")
+ }
+
+ let uninstalledConfig = (try? String(contentsOf: configURL)) ?? ""
+ expect(
+ uninstalledConfig.contains("usr") && uninstalledConfig.contains("true"),
+ "移除 Hooks 时必须保留用户原有命令"
+ )
+ expect(
+ !uninstalledConfig.contains(AgentIntegrationManager.adapterID),
+ "移除后不应残留 AgentAwake Hook"
+ )
+ expect(
+ !fileManager.fileExists(atPath: manager.installedHelperURL.path),
+ "没有集成使用 helper 时应移除稳定副本"
+ )
+
+ do {
+ try manager.install(.claude)
+ failures.append("未检测到 Claude 时不应创建配置")
+ } catch {
+ expect(
+ manager.inspect(.claude).state == .notDetected,
+ "未检测到 Claude 时状态应保持未检测到"
+ )
+ }
+
+ let claudeDirectory = root.appendingPathComponent(
+ ".claude",
+ isDirectory: true
+ )
+ try? fileManager.createDirectory(
+ at: claudeDirectory,
+ withIntermediateDirectories: true
+ )
+ let invalidClaudeConfig = claudeDirectory.appendingPathComponent(
+ "settings.json"
+ )
+ let invalidData = Data(#"{"hooks":"not-an-object"}"#.utf8)
+ try? invalidData.write(to: invalidClaudeConfig)
+
+ if case .invalidConfiguration = manager.inspect(.claude).state {
+ expect(true, "异常 Claude 配置应被识别")
+ } else {
+ failures.append("异常 Claude 配置应显示配置错误")
+ }
+
+ do {
+ try manager.install(.claude)
+ failures.append("异常 Claude 配置不应被覆盖")
+ } catch {
+ expect(
+ (try? Data(contentsOf: invalidClaudeConfig)) == invalidData,
+ "安装失败后必须保持异常配置原文不变"
+ )
+ expect(
+ !fileManager.fileExists(atPath: manager.installedHelperURL.path),
+ "配置异常时不应留下未使用的 helper"
+ )
+ }
+}
+
testProtectionSession()
testAgentActivityParsing()
testHookActivityStore()
testHookOverridesTranscriptFallback()
+testSelectiveIntegrationSetup()
if failures.isEmpty {
let detector = SystemAgentDetector()
diff --git a/Sources/AgentAwakeSetupCore/AgentIntegrationManager.swift b/Sources/AgentAwakeSetupCore/AgentIntegrationManager.swift
new file mode 100644
index 0000000..73a4ba6
--- /dev/null
+++ b/Sources/AgentAwakeSetupCore/AgentIntegrationManager.swift
@@ -0,0 +1,572 @@
+import Foundation
+
+public enum AgentIntegrationProvider: String, CaseIterable, Identifiable, Sendable {
+ case codex
+ case claude
+
+ public var id: String { rawValue }
+
+ public var displayName: String {
+ switch self {
+ case .codex:
+ return "Codex"
+ case .claude:
+ return "Claude"
+ }
+ }
+
+ fileprivate var homeDirectoryName: String {
+ ".\(rawValue)"
+ }
+
+ fileprivate var configurationFilename: String {
+ switch self {
+ case .codex:
+ return "hooks.json"
+ case .claude:
+ return "settings.json"
+ }
+ }
+
+ fileprivate var activityDirectoryComponents: [String] {
+ switch self {
+ case .codex:
+ return ["sessions"]
+ case .claude:
+ return ["projects"]
+ }
+ }
+
+ fileprivate var events: [String] {
+ switch self {
+ case .codex:
+ return [
+ "UserPromptSubmit",
+ "PreToolUse",
+ "PermissionRequest",
+ "PostToolUse",
+ "Stop",
+ "SessionEnd"
+ ]
+ case .claude:
+ return [
+ "UserPromptSubmit",
+ "PreToolUse",
+ "PermissionRequest",
+ "PostToolUse",
+ "PostToolUseFailure",
+ "Stop",
+ "StopFailure",
+ "SessionEnd"
+ ]
+ }
+ }
+}
+
+public enum AgentIntegrationState: Equatable, Sendable {
+ case notDetected
+ case available
+ case installed
+ case needsRepair
+ case invalidConfiguration(String)
+}
+
+public struct AgentIntegrationSnapshot: Identifiable, Equatable, Sendable {
+ public let provider: AgentIntegrationProvider
+ public let state: AgentIntegrationState
+ public let hasLocalActivityData: Bool
+ public let configurationPath: String
+
+ public var id: String { provider.id }
+
+ public init(
+ provider: AgentIntegrationProvider,
+ state: AgentIntegrationState,
+ hasLocalActivityData: Bool,
+ configurationPath: String
+ ) {
+ self.provider = provider
+ self.state = state
+ self.hasLocalActivityData = hasLocalActivityData
+ self.configurationPath = configurationPath
+ }
+}
+
+public enum AgentIntegrationError: LocalizedError {
+ case providerNotDetected(String)
+ case helperMissing(String)
+ case invalidJSONObject(String)
+ case invalidHooks(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case let .providerNotDetected(provider):
+ return "尚未检测到 \(provider),未创建新的配置目录。"
+ case let .helperMissing(path):
+ return "找不到 AgentAwakeHook:\(path)"
+ case let .invalidJSONObject(path):
+ return "配置不是有效的 JSON 对象,未修改:\(path)"
+ case let .invalidHooks(path):
+ return "配置中的 hooks 结构无法安全合并,未修改:\(path)"
+ }
+ }
+}
+
+public final class AgentIntegrationManager: @unchecked Sendable {
+ public static let adapterID = "com.raymond.agentawake"
+
+ public let homeDirectory: URL
+ public let bundledHelperURL: URL
+
+ private let fileManager: FileManager
+ private let lock = NSLock()
+
+ public init(
+ homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
+ bundledHelperURL: URL,
+ fileManager: FileManager = .default
+ ) {
+ self.homeDirectory = homeDirectory
+ self.bundledHelperURL = bundledHelperURL
+ self.fileManager = fileManager
+ }
+
+ public var installedHelperURL: URL {
+ homeDirectory
+ .appendingPathComponent("Library", isDirectory: true)
+ .appendingPathComponent("Application Support", isDirectory: true)
+ .appendingPathComponent("AgentAwake", isDirectory: true)
+ .appendingPathComponent("bin", isDirectory: true)
+ .appendingPathComponent("AgentAwakeHook")
+ }
+
+ public func detectedProviders() -> [AgentIntegrationProvider] {
+ AgentIntegrationProvider.allCases.filter(isDetected)
+ }
+
+ public func inspectAll() -> [AgentIntegrationSnapshot] {
+ AgentIntegrationProvider.allCases.map(inspect)
+ }
+
+ public func inspect(
+ _ provider: AgentIntegrationProvider
+ ) -> AgentIntegrationSnapshot {
+ lock.lock()
+ defer { lock.unlock() }
+ return inspectUnlocked(provider)
+ }
+
+ public func install(_ provider: AgentIntegrationProvider) throws {
+ lock.lock()
+ defer { lock.unlock() }
+
+ guard isDetected(provider) else {
+ throw AgentIntegrationError.providerNotDetected(
+ provider.displayName
+ )
+ }
+
+ let configURL = configurationURL(for: provider)
+ var root = try loadJSONObject(
+ at: configURL,
+ defaultValue: provider == .codex
+ ? ["description": "Personal lifecycle hooks."]
+ : [:]
+ )
+ try updateHooks(
+ in: &root,
+ provider: provider,
+ handler: handler(
+ for: provider,
+ helperPath: installedHelperURL.path
+ )
+ )
+
+ do {
+ _ = try installStableHelper()
+ try writeJSONObject(root, to: configURL)
+ } catch {
+ if !hasAnyConfiguredIntegrationUnlocked() {
+ try? fileManager.removeItem(at: installedHelperURL)
+ }
+ throw error
+ }
+ }
+
+ public func uninstall(_ provider: AgentIntegrationProvider) throws {
+ lock.lock()
+ defer { lock.unlock() }
+
+ let configURL = configurationURL(for: provider)
+ if fileManager.fileExists(atPath: configURL.path) {
+ var root = try loadJSONObject(at: configURL, defaultValue: [:])
+ let isConfigured = try hasAgentAwakeHandler(
+ in: root,
+ provider: provider,
+ configPath: configURL.path
+ )
+ if isConfigured {
+ try updateHooks(in: &root, provider: provider, handler: nil)
+ try writeJSONObject(root, to: configURL)
+ }
+ }
+
+ if !hasAnyConfiguredIntegrationUnlocked() {
+ try? fileManager.removeItem(at: installedHelperURL)
+ }
+ }
+
+ public func configurationURL(
+ for provider: AgentIntegrationProvider
+ ) -> URL {
+ providerHomeURL(provider)
+ .appendingPathComponent(provider.configurationFilename)
+ }
+
+ private func inspectUnlocked(
+ _ provider: AgentIntegrationProvider
+ ) -> AgentIntegrationSnapshot {
+ let configURL = configurationURL(for: provider)
+ let hasLocalActivityData = fileManager.fileExists(
+ atPath: activityDirectoryURL(provider).path
+ )
+
+ guard isDetected(provider) else {
+ return AgentIntegrationSnapshot(
+ provider: provider,
+ state: .notDetected,
+ hasLocalActivityData: false,
+ configurationPath: configURL.path
+ )
+ }
+
+ guard fileManager.fileExists(atPath: configURL.path) else {
+ return AgentIntegrationSnapshot(
+ provider: provider,
+ state: .available,
+ hasLocalActivityData: hasLocalActivityData,
+ configurationPath: configURL.path
+ )
+ }
+
+ do {
+ let root = try loadJSONObject(at: configURL, defaultValue: [:])
+ let configured = try hasAgentAwakeHandler(
+ in: root,
+ provider: provider,
+ configPath: configURL.path
+ )
+ guard configured else {
+ return AgentIntegrationSnapshot(
+ provider: provider,
+ state: .available,
+ hasLocalActivityData: hasLocalActivityData,
+ configurationPath: configURL.path
+ )
+ }
+
+ let helperIsCurrent = helperMatchesBundledVersion()
+ return AgentIntegrationSnapshot(
+ provider: provider,
+ state: helperIsCurrent ? .installed : .needsRepair,
+ hasLocalActivityData: hasLocalActivityData,
+ configurationPath: configURL.path
+ )
+ } catch {
+ return AgentIntegrationSnapshot(
+ provider: provider,
+ state: .invalidConfiguration(error.localizedDescription),
+ hasLocalActivityData: hasLocalActivityData,
+ configurationPath: configURL.path
+ )
+ }
+ }
+
+ private func isDetected(_ provider: AgentIntegrationProvider) -> Bool {
+ fileManager.fileExists(atPath: providerHomeURL(provider).path)
+ }
+
+ private func providerHomeURL(
+ _ provider: AgentIntegrationProvider
+ ) -> URL {
+ homeDirectory.appendingPathComponent(
+ provider.homeDirectoryName,
+ isDirectory: true
+ )
+ }
+
+ private func activityDirectoryURL(
+ _ provider: AgentIntegrationProvider
+ ) -> URL {
+ provider.activityDirectoryComponents.reduce(
+ providerHomeURL(provider)
+ ) { partialURL, component in
+ partialURL.appendingPathComponent(component, isDirectory: true)
+ }
+ }
+
+ private func helperMatchesBundledVersion() -> Bool {
+ guard fileManager.isExecutableFile(atPath: installedHelperURL.path),
+ let bundledData = try? Data(contentsOf: bundledHelperURL),
+ let installedData = try? Data(contentsOf: installedHelperURL)
+ else {
+ return false
+ }
+
+ return bundledData == installedData
+ }
+
+ private func installStableHelper() throws -> URL {
+ guard fileManager.isExecutableFile(atPath: bundledHelperURL.path) else {
+ throw AgentIntegrationError.helperMissing(bundledHelperURL.path)
+ }
+
+ let destinationURL = installedHelperURL
+ try fileManager.createDirectory(
+ at: destinationURL.deletingLastPathComponent(),
+ withIntermediateDirectories: true
+ )
+
+ if bundledHelperURL.standardizedFileURL
+ != destinationURL.standardizedFileURL
+ {
+ let data = try Data(contentsOf: bundledHelperURL)
+ try data.write(to: destinationURL, options: .atomic)
+ }
+
+ try fileManager.setAttributes(
+ [.posixPermissions: 0o755],
+ ofItemAtPath: destinationURL.path
+ )
+ return destinationURL
+ }
+
+ private func loadJSONObject(
+ at url: URL,
+ defaultValue: [String: Any]
+ ) throws -> [String: Any] {
+ guard fileManager.fileExists(atPath: url.path) else {
+ return defaultValue
+ }
+
+ let data = try Data(contentsOf: url)
+ guard let object = try JSONSerialization.jsonObject(with: data)
+ as? [String: Any]
+ else {
+ throw AgentIntegrationError.invalidJSONObject(url.path)
+ }
+
+ return object
+ }
+
+ private func hookGroups(
+ from value: Any?,
+ configPath: String
+ ) throws -> [[String: Any]] {
+ guard let value else {
+ return []
+ }
+ guard let groups = value as? [[String: Any]] else {
+ throw AgentIntegrationError.invalidHooks(configPath)
+ }
+ return groups
+ }
+
+ private func isAgentAwakeHandler(_ handler: [String: Any]) -> Bool {
+ if let command = handler["command"] as? String,
+ command.contains("--adapter-id \(Self.adapterID)")
+ {
+ return true
+ }
+
+ if let arguments = handler["args"] as? [String],
+ let markerIndex = arguments.firstIndex(of: "--adapter-id"),
+ arguments.indices.contains(markerIndex + 1),
+ arguments[markerIndex + 1] == Self.adapterID
+ {
+ return true
+ }
+
+ return false
+ }
+
+ private func removingAgentAwakeHandlers(
+ from groups: [[String: Any]],
+ configPath: String
+ ) throws -> [[String: Any]] {
+ try groups.compactMap { originalGroup in
+ var group = originalGroup
+ guard let handlersValue = group["hooks"] else {
+ return group
+ }
+ guard let handlers = handlersValue as? [[String: Any]] else {
+ throw AgentIntegrationError.invalidHooks(configPath)
+ }
+
+ let retained = handlers.filter { !isAgentAwakeHandler($0) }
+ guard !retained.isEmpty else {
+ return nil
+ }
+
+ group["hooks"] = retained
+ return group
+ }
+ }
+
+ private func hasAgentAwakeHandler(
+ in root: [String: Any],
+ provider: AgentIntegrationProvider,
+ configPath: String
+ ) throws -> Bool {
+ guard let hooksValue = root["hooks"] else {
+ return false
+ }
+ guard let hooks = hooksValue as? [String: Any] else {
+ throw AgentIntegrationError.invalidHooks(configPath)
+ }
+
+ for event in provider.events {
+ let groups = try hookGroups(
+ from: hooks[event],
+ configPath: configPath
+ )
+ for group in groups {
+ guard let handlersValue = group["hooks"] else {
+ continue
+ }
+ guard let handlers = handlersValue as? [[String: Any]] else {
+ throw AgentIntegrationError.invalidHooks(configPath)
+ }
+ if handlers.contains(where: isAgentAwakeHandler) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ private func updateHooks(
+ in root: inout [String: Any],
+ provider: AgentIntegrationProvider,
+ handler: [String: Any]?
+ ) throws {
+ let configPath = configurationURL(for: provider).path
+ let existingHooks = root["hooks"] as? [String: Any]
+ if root["hooks"] != nil, existingHooks == nil {
+ throw AgentIntegrationError.invalidHooks(configPath)
+ }
+
+ var hooks = existingHooks ?? [:]
+ for event in provider.events {
+ let groups = try hookGroups(
+ from: hooks[event],
+ configPath: configPath
+ )
+ var updated = try removingAgentAwakeHandlers(
+ from: groups,
+ configPath: configPath
+ )
+
+ if let handler {
+ updated.append(["hooks": [handler]])
+ }
+
+ if updated.isEmpty {
+ hooks.removeValue(forKey: event)
+ } else {
+ hooks[event] = updated
+ }
+ }
+ root["hooks"] = hooks
+ }
+
+ private func createBackupIfNeeded(at url: URL) throws {
+ guard fileManager.fileExists(atPath: url.path) else {
+ return
+ }
+
+ let backupURL = URL(fileURLWithPath: url.path + ".agentawake-backup")
+ guard !fileManager.fileExists(atPath: backupURL.path) else {
+ return
+ }
+ try fileManager.copyItem(at: url, to: backupURL)
+ }
+
+ private func writeJSONObject(
+ _ object: [String: Any],
+ to url: URL
+ ) throws {
+ try fileManager.createDirectory(
+ at: url.deletingLastPathComponent(),
+ withIntermediateDirectories: true
+ )
+ try createBackupIfNeeded(at: url)
+
+ var data = try JSONSerialization.data(
+ withJSONObject: object,
+ options: [.prettyPrinted, .sortedKeys]
+ )
+ data.append(0x0A)
+ try data.write(to: url, options: .atomic)
+ try? fileManager.setAttributes(
+ [.posixPermissions: 0o600],
+ ofItemAtPath: url.path
+ )
+ }
+
+ private func handler(
+ for provider: AgentIntegrationProvider,
+ helperPath: String
+ ) -> [String: Any] {
+ switch provider {
+ case .codex:
+ let command = [
+ singleQuotedShellArgument(helperPath),
+ "--provider codex",
+ "--adapter-id \(Self.adapterID)"
+ ].joined(separator: " ")
+ return [
+ "type": "command",
+ "command": command,
+ "timeout": 3
+ ]
+
+ case .claude:
+ return [
+ "type": "command",
+ "command": helperPath,
+ "args": [
+ "--provider",
+ "claude",
+ "--adapter-id",
+ Self.adapterID
+ ],
+ "timeout": 3
+ ]
+ }
+ }
+
+ private func singleQuotedShellArgument(_ value: String) -> String {
+ "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
+ }
+
+ private func hasAnyConfiguredIntegrationUnlocked() -> Bool {
+ AgentIntegrationProvider.allCases.contains { provider in
+ let configURL = configurationURL(for: provider)
+ guard fileManager.fileExists(atPath: configURL.path),
+ let root = try? loadJSONObject(
+ at: configURL,
+ defaultValue: [:]
+ )
+ else {
+ return false
+ }
+ return (try? hasAgentAwakeHandler(
+ in: root,
+ provider: provider,
+ configPath: configURL.path
+ )) == true
+ }
+ }
+}
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
new file mode 100644
index 0000000..be0dcb9
--- /dev/null
+++ b/docs/RELEASING.md
@@ -0,0 +1,68 @@
+# AgentAwake 发布说明
+
+AgentAwake 的公开发行物是经过 Developer ID 签名和 Apple 公证的 Universal 2
+macOS App。公开 Release 不应上传 ad-hoc 签名的本地产物。
+
+## 发布前提
+
+- Apple Developer Program 账号;
+- `Developer ID Application` 证书;
+- Apple 公证所需的 Apple ID、Team ID 与 App 专用密码;
+- GitHub Actions 中已经配置下列 Secrets:
+ - `DEVELOPER_ID_P12_BASE64`
+ - `DEVELOPER_ID_P12_PASSWORD`
+ - `DEVELOPER_ID_APPLICATION`
+ - `BUILD_KEYCHAIN_PASSWORD`
+ - `NOTARY_APPLE_ID`
+ - `NOTARY_APP_PASSWORD`
+ - `APPLE_TEAM_ID`
+
+## 本地验证
+
+```bash
+swift run --disable-sandbox AgentAwakeSelfTest
+./scripts/build-app.sh release universal
+codesign --verify --deep --strict --verbose=2 dist/AgentAwake.app
+file dist/AgentAwake.app/Contents/MacOS/AgentAwake
+```
+
+本地构建使用 ad-hoc 签名,只能用于开发验证,不能作为公开下载包。
+
+## 正式打包
+
+先把公证凭据保存为钥匙串 Profile:
+
+```bash
+xcrun notarytool store-credentials agentawake-notary
+```
+
+然后执行:
+
+```bash
+DEVELOPER_ID_APPLICATION="Developer ID Application: ..." \
+NOTARY_KEYCHAIN_PROFILE="agentawake-notary" \
+./scripts/package-release.sh
+```
+
+如果 Profile 保存于非默认钥匙串,同时传入
+`NOTARY_KEYCHAIN=/path/to/keychain-db`。
+
+脚本会依次完成:Universal 2 构建、嵌套 helper 签名、App 签名、DMG 制作、
+Apple 公证、票据装订、Gatekeeper 验证、ZIP 制作和 SHA-256 校验。
+
+## GitHub Release
+
+1. 确认 `Resources/Info.plist`、README 与 Changelog 版本一致。
+2. 确认 `main` 的 CI 通过。
+3. 创建与版本一致的 Tag,例如 `v0.4.0`。
+4. 推送 Tag 后,`.github/workflows/release.yml` 自动发布 DMG、ZIP 和校验文件。
+5. 在一台干净的 Mac 上从 GitHub Release 真实下载并完成最终验收。
+
+## 最终验收
+
+- 用户不需要 Xcode、Swift、Codex、Claude 或 Cloud;
+- ZIP 解压后可直接双击 App,DMG 可按标准方式拖入“应用程序”;
+- Gatekeeper 认可开发者和公证票据;
+- 主程序与两个 helper 均包含 `arm64` 和 `x86_64`;
+- 不安装 Hooks 时,定时模式和本地日志兜底仍可使用;
+- Hooks 只能由用户在设置中明确选择安装或移除。
diff --git a/scripts/build-app.sh b/scripts/build-app.sh
index f232ce3..4ac1d4a 100755
--- a/scripts/build-app.sh
+++ b/scripts/build-app.sh
@@ -4,46 +4,104 @@ set -euo pipefail
PROJECT_DIR="${0:A:h:h}"
CONFIGURATION="${1:-release}"
+ARCHITECTURE_MODE="${2:-universal}"
APP_DIR="$PROJECT_DIR/dist/AgentAwake.app"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
HELPERS_DIR="$CONTENTS_DIR/Helpers"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
APP_ICON="$PROJECT_DIR/Resources/AppIcon.icns"
+BUILD_ROOT="$PROJECT_DIR/.build/app-bundle"
+EXECUTABLES=(AgentAwake AgentAwakeHook AgentAwakeHookSetup)
export CLANG_MODULE_CACHE_PATH="/private/tmp/agentawake-clang-cache"
export SWIFT_MODULECACHE_PATH="/private/tmp/agentawake-swift-cache"
+if [[ "$CONFIGURATION" != "debug" && "$CONFIGURATION" != "release" ]]; then
+ echo "Configuration must be debug or release." >&2
+ exit 64
+fi
+
+if [[ "$ARCHITECTURE_MODE" != "native" \
+ && "$ARCHITECTURE_MODE" != "universal" ]]; then
+ echo "Architecture mode must be native or universal." >&2
+ exit 64
+fi
+
if [[ ! -f "$APP_ICON" ]]; then
"$PROJECT_DIR/scripts/build-icon.sh"
fi
-/usr/bin/swift build \
- --package-path "$PROJECT_DIR" \
- --configuration "$CONFIGURATION" \
- --disable-sandbox
+build_for_architecture() {
+ local architecture="$1"
+ local triple="${architecture}-apple-macosx13.0"
+ local scratch_path="$BUILD_ROOT/$architecture"
+ /usr/bin/swift build \
+ --package-path "$PROJECT_DIR" \
+ --configuration "$CONFIGURATION" \
+ --triple "$triple" \
+ --scratch-path "$scratch_path" \
+ --disable-sandbox >&2
+
+ /usr/bin/swift build \
+ --package-path "$PROJECT_DIR" \
+ --configuration "$CONFIGURATION" \
+ --triple "$triple" \
+ --scratch-path "$scratch_path" \
+ --disable-sandbox \
+ --show-bin-path
+}
+
+/bin/rm -rf "$APP_DIR"
/bin/mkdir -p "$MACOS_DIR" "$HELPERS_DIR" "$RESOURCES_DIR"
-/bin/cp \
- "$PROJECT_DIR/.build/$CONFIGURATION/AgentAwake" \
- "$MACOS_DIR/AgentAwake"
+
+if [[ "$ARCHITECTURE_MODE" == "universal" ]]; then
+ ARM64_BIN_DIR="$(build_for_architecture arm64)"
+ X86_64_BIN_DIR="$(build_for_architecture x86_64)"
+
+ for executable in $EXECUTABLES; do
+ destination="$MACOS_DIR/$executable"
+ if [[ "$executable" != "AgentAwake" ]]; then
+ destination="$HELPERS_DIR/$executable"
+ fi
+
+ /usr/bin/lipo -create \
+ "$ARM64_BIN_DIR/$executable" \
+ "$X86_64_BIN_DIR/$executable" \
+ -output "$destination"
+ done
+else
+ /usr/bin/swift build \
+ --package-path "$PROJECT_DIR" \
+ --configuration "$CONFIGURATION" \
+ --disable-sandbox
+ NATIVE_BIN_DIR="$PROJECT_DIR/.build/$CONFIGURATION"
+
+ /bin/cp "$NATIVE_BIN_DIR/AgentAwake" "$MACOS_DIR/AgentAwake"
+ /bin/cp "$NATIVE_BIN_DIR/AgentAwakeHook" "$HELPERS_DIR/AgentAwakeHook"
+ /bin/cp \
+ "$NATIVE_BIN_DIR/AgentAwakeHookSetup" \
+ "$HELPERS_DIR/AgentAwakeHookSetup"
+fi
+
/bin/cp "$PROJECT_DIR/Resources/Info.plist" "$CONTENTS_DIR/Info.plist"
/bin/cp "$APP_ICON" "$RESOURCES_DIR/AppIcon.icns"
-/bin/chmod 755 "$MACOS_DIR/AgentAwake"
-/bin/cp \
- "$PROJECT_DIR/.build/$CONFIGURATION/AgentAwakeHook" \
- "$HELPERS_DIR/AgentAwakeHook"
-/bin/cp \
- "$PROJECT_DIR/.build/$CONFIGURATION/AgentAwakeHookSetup" \
- "$HELPERS_DIR/AgentAwakeHookSetup"
/bin/chmod 755 \
+ "$MACOS_DIR/AgentAwake" \
"$HELPERS_DIR/AgentAwakeHook" \
"$HELPERS_DIR/AgentAwakeHookSetup"
+for helper in "$HELPERS_DIR/AgentAwakeHook" \
+ "$HELPERS_DIR/AgentAwakeHookSetup"; do
+ /usr/bin/codesign --force --sign - "$helper"
+done
+
/usr/bin/codesign \
--force \
- --deep \
--sign - \
"$APP_DIR"
+/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_DIR"
+
echo "$APP_DIR"
diff --git a/scripts/package-release.sh b/scripts/package-release.sh
new file mode 100755
index 0000000..bd48be8
--- /dev/null
+++ b/scripts/package-release.sh
@@ -0,0 +1,111 @@
+#!/bin/zsh
+
+set -euo pipefail
+
+PROJECT_DIR="${0:A:h:h}"
+APP_DIR="$PROJECT_DIR/dist/AgentAwake.app"
+RELEASE_DIR="$PROJECT_DIR/dist/release"
+STAGING_DIR="$PROJECT_DIR/dist/dmg-staging"
+INFO_PLIST="$PROJECT_DIR/Resources/Info.plist"
+SIGNING_IDENTITY="${DEVELOPER_ID_APPLICATION:-}"
+NOTARY_PROFILE="${NOTARY_KEYCHAIN_PROFILE:-}"
+NOTARY_KEYCHAIN_PATH="${NOTARY_KEYCHAIN:-}"
+VERSION="$(/usr/libexec/PlistBuddy \
+ -c 'Print :CFBundleShortVersionString' \
+ "$INFO_PLIST")"
+DMG_PATH="$RELEASE_DIR/AgentAwake-$VERSION.dmg"
+ZIP_PATH="$RELEASE_DIR/AgentAwake-$VERSION.zip"
+CHECKSUM_PATH="$RELEASE_DIR/AgentAwake-$VERSION.sha256"
+
+if [[ -z "$SIGNING_IDENTITY" ]]; then
+ echo "DEVELOPER_ID_APPLICATION is required for a public release." >&2
+ exit 78
+fi
+
+if [[ -z "$NOTARY_PROFILE" ]]; then
+ echo "NOTARY_KEYCHAIN_PROFILE is required for a public release." >&2
+ exit 78
+fi
+
+"$PROJECT_DIR/scripts/build-app.sh" release universal
+
+for helper in \
+ "$APP_DIR/Contents/Helpers/AgentAwakeHook" \
+ "$APP_DIR/Contents/Helpers/AgentAwakeHookSetup"; do
+ /usr/bin/codesign \
+ --force \
+ --options runtime \
+ --timestamp \
+ --sign "$SIGNING_IDENTITY" \
+ "$helper"
+done
+
+/usr/bin/codesign \
+ --force \
+ --options runtime \
+ --timestamp \
+ --sign "$SIGNING_IDENTITY" \
+ "$APP_DIR"
+
+/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_DIR"
+
+/bin/rm -rf "$RELEASE_DIR" "$STAGING_DIR"
+/bin/mkdir -p "$RELEASE_DIR" "$STAGING_DIR"
+/usr/bin/ditto "$APP_DIR" "$STAGING_DIR/AgentAwake.app"
+/bin/ln -s /Applications "$STAGING_DIR/Applications"
+
+/usr/bin/hdiutil create \
+ -volname "AgentAwake" \
+ -srcfolder "$STAGING_DIR" \
+ -ov \
+ -format UDZO \
+ "$DMG_PATH"
+
+/usr/bin/codesign \
+ --force \
+ --timestamp \
+ --sign "$SIGNING_IDENTITY" \
+ "$DMG_PATH"
+
+NOTARY_ARGUMENTS=(--keychain-profile "$NOTARY_PROFILE")
+if [[ -n "$NOTARY_KEYCHAIN_PATH" ]]; then
+ NOTARY_ARGUMENTS+=(--keychain "$NOTARY_KEYCHAIN_PATH")
+fi
+
+/usr/bin/xcrun notarytool submit \
+ "$DMG_PATH" \
+ "${NOTARY_ARGUMENTS[@]}" \
+ --wait
+/usr/bin/xcrun stapler staple "$DMG_PATH"
+/usr/bin/xcrun stapler validate "$DMG_PATH"
+/usr/bin/xcrun stapler staple "$APP_DIR"
+/usr/bin/xcrun stapler validate "$APP_DIR"
+/usr/bin/ditto \
+ -c \
+ -k \
+ --sequesterRsrc \
+ --keepParent \
+ "$APP_DIR" \
+ "$ZIP_PATH"
+/usr/sbin/spctl --assess --type execute --verbose=2 "$APP_DIR"
+/usr/sbin/spctl \
+ --assess \
+ --type open \
+ --context context:primary-signature \
+ --verbose=2 \
+ "$DMG_PATH"
+/usr/bin/hdiutil verify "$DMG_PATH"
+
+(
+ cd "$RELEASE_DIR"
+ /usr/bin/shasum -a 256 \
+ "${DMG_PATH:t}" \
+ "${ZIP_PATH:t}" \
+ > "${CHECKSUM_PATH:t}"
+)
+
+/bin/rm -rf "$STAGING_DIR"
+
+echo "$DMG_PATH"
+echo "$ZIP_PATH"
+echo "$CHECKSUM_PATH"
From ffc0d8463f3aeea6269482828f1bbfa1bdc2c146 Mon Sep 17 00:00:00 2001
From: Raymond
Date: Fri, 31 Jul 2026 22:39:57 +0800
Subject: [PATCH 2/3] Make releases DMG-only
---
.github/workflows/release.yml | 1 -
CHANGELOG.md | 4 ++--
README.md | 8 +++++--
README_EN.md | 12 ++++++----
docs/RELEASING.md | 45 +++++++++++++++++++++++++++++------
scripts/package-release.sh | 16 +------------
6 files changed, 55 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8ffbe00..b3d26f1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -78,7 +78,6 @@ jobs:
run: |
gh release create "$GITHUB_REF_NAME" \
dist/release/AgentAwake-*.dmg \
- dist/release/AgentAwake-*.zip \
dist/release/AgentAwake-*.sha256 \
--title "AgentAwake ${GITHUB_REF_NAME#v}" \
--generate-notes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dfccf0..4c4bdab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,8 +21,8 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
In-app settings for the login item and optional Codex/Claude Hooks.
- 首次启动自动展示菜单栏面板;定时模式无需任何 Agent 或额外配置。
First launch reveals the menu bar panel; timed modes need no agent or setup.
-- Developer ID 签名、Apple 公证、DMG/ZIP 制作和 GitHub Release 自动化。
- Developer ID signing, notarization, DMG/ZIP packaging, and release automation.
+- Developer ID 签名、Apple 公证、DMG 制作和 GitHub Release 自动化。
+ Developer ID signing, notarization, DMG packaging, and release automation.
### Changed / 变更
diff --git a/README.md b/README.md
index e7850e4..11c3ae7 100644
--- a/README.md
+++ b/README.md
@@ -72,8 +72,12 @@ AgentAwake 是一个下载后即可使用的轻量 macOS 菜单栏工具。
前往 [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-- **最少操作**:下载 ZIP,解压后双击 `AgentAwake.app`;
-- **标准安装**:下载 DMG,将 App 拖入“应用程序”后双击启动。
+1. 下载 `AgentAwake-x.y.z.dmg` 并双击打开;
+2. 在打开的窗口中双击 `AgentAwake.app`,即可直接使用;
+3. 如果准备长期使用,建议将 App 拖入“应用程序”,尤其是在启用“登录时启动”前。
+
+拖入“应用程序”不是首次使用的前置条件。首次启动时,macOS 仍会显示一次标准的
+“从互联网下载”安全确认;确认打开后不需要再执行终端命令或手动部署。
首次启动后,AgentAwake 会自动展开一次菜单栏面板,之后只保留菜单栏图标,
不显示 Dock 图标。三个定时模式此时已经可以使用,不需要继续配置。
diff --git a/README_EN.md b/README_EN.md
index ef4cad0..647fd72 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -77,10 +77,14 @@ or keeps a persistent `caffeinate` or shell process running.
Open [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-- **Fewest steps:** download the ZIP, extract it, and double-click
- `AgentAwake.app`.
-- **Standard installation:** download the DMG, drag the app to Applications,
- and double-click it.
+1. Download and open `AgentAwake-x.y.z.dmg`.
+2. Double-click `AgentAwake.app` in the mounted window to use it immediately.
+3. For long-term use, drag the app to Applications, especially before enabling
+ Launch at Login.
+
+Moving the app to Applications is not required for the first run. macOS still
+shows its standard downloaded-from-the-internet confirmation once; after you
+confirm, no Terminal commands or manual deployment are needed.
On its first launch, AgentAwake opens its menu bar panel once. It then remains
in the menu bar without a Dock icon. All three timed modes are immediately
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index be0dcb9..ada6e04 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -1,14 +1,45 @@
# AgentAwake 发布说明
-AgentAwake 的公开发行物是经过 Developer ID 签名和 Apple 公证的 Universal 2
-macOS App。公开 Release 不应上传 ad-hoc 签名的本地产物。
+AgentAwake 的公开发行物是一个经过 Developer ID 签名和 Apple 公证的 Universal 2
+DMG,内部包含原生 macOS App。公开 Release 不应上传 ZIP 或 ad-hoc 签名的本地产物。
## 发布前提
- Apple Developer Program 账号;
- `Developer ID Application` 证书;
-- Apple 公证所需的 Apple ID、Team ID 与 App 专用密码;
-- GitHub Actions 中已经配置下列 Secrets:
+- Apple 公证所需的 Apple Account、Team ID 与 App 专用密码。
+
+### 获取 Developer ID 身份
+
+1. 加入 [Apple Developer Program](https://developer.apple.com/programs/enroll/);
+ 个人账号的持有人就是 Account Holder。
+2. 在“钥匙串访问”中选择“钥匙串访问 > 证书助理 > 从证书颁发机构请求证书”,
+ 将 CSR 保存到磁盘。Apple 的完整步骤见
+ [Create a certificate signing request](https://developer.apple.com/help/account/certificates/create-a-certificate-signing-request)。
+3. 登录 Apple Developer 的 `Certificates, Identifiers & Profiles`,进入
+ `Certificates`,点击 `+`,在 `Software` 下选择 `Developer ID`,然后选择
+ `Developer ID Application` 并上传 CSR。不要选择 `Developer ID Installer`;
+ 后者只用于 `.pkg` 安装器。
+4. 下载 `.cer` 并双击安装。钥匙串的“我的证书”中应出现
+ `Developer ID Application: 名称 (TEAMID)`,展开后还能看到对应私钥。
+
+Developer ID 证书的官方步骤见
+[Developer ID certificates](https://developer.apple.com/help/account/certificates/create-developer-id-certificates/)。
+Team ID 可在 Apple Developer 账号的 `Membership details` 中找到。
+
+### 公证凭据
+
+在 [account.apple.com](https://account.apple.com/) 的“登录与安全 > App 专用密码”
+生成一个专用密码,然后使用 `notarytool store-credentials` 保存到本机钥匙串。
+不要在 Issue、PR、聊天或仓库文件中粘贴 Apple Account 密码、App 专用密码、
+私钥、`.p12` 文件或 `.p12` 密码。
+
+### GitHub Actions 自动发布(可选)
+
+第一次正式发布可以完全在本机完成,不要求先配置 GitHub Secrets。若要让 Tag
+自动触发签名、公证和 Release,需要把证书与私钥导出为受密码保护的 `.p12`,
+并由仓库管理员在 GitHub Actions 中配置:
+
- `DEVELOPER_ID_P12_BASE64`
- `DEVELOPER_ID_P12_PASSWORD`
- `DEVELOPER_ID_APPLICATION`
@@ -48,20 +79,20 @@ NOTARY_KEYCHAIN_PROFILE="agentawake-notary" \
`NOTARY_KEYCHAIN=/path/to/keychain-db`。
脚本会依次完成:Universal 2 构建、嵌套 helper 签名、App 签名、DMG 制作、
-Apple 公证、票据装订、Gatekeeper 验证、ZIP 制作和 SHA-256 校验。
+Apple 公证、票据装订、Gatekeeper 验证和 SHA-256 校验。
## GitHub Release
1. 确认 `Resources/Info.plist`、README 与 Changelog 版本一致。
2. 确认 `main` 的 CI 通过。
3. 创建与版本一致的 Tag,例如 `v0.4.0`。
-4. 推送 Tag 后,`.github/workflows/release.yml` 自动发布 DMG、ZIP 和校验文件。
+4. 推送 Tag 后,`.github/workflows/release.yml` 自动发布 DMG 和校验文件。
5. 在一台干净的 Mac 上从 GitHub Release 真实下载并完成最终验收。
## 最终验收
- 用户不需要 Xcode、Swift、Codex、Claude 或 Cloud;
-- ZIP 解压后可直接双击 App,DMG 可按标准方式拖入“应用程序”;
+- DMG 打开后可直接双击 App;拖入“应用程序”仅作为长期使用的推荐步骤;
- Gatekeeper 认可开发者和公证票据;
- 主程序与两个 helper 均包含 `arm64` 和 `x86_64`;
- 不安装 Hooks 时,定时模式和本地日志兜底仍可使用;
diff --git a/scripts/package-release.sh b/scripts/package-release.sh
index bd48be8..8171685 100755
--- a/scripts/package-release.sh
+++ b/scripts/package-release.sh
@@ -14,7 +14,6 @@ VERSION="$(/usr/libexec/PlistBuddy \
-c 'Print :CFBundleShortVersionString' \
"$INFO_PLIST")"
DMG_PATH="$RELEASE_DIR/AgentAwake-$VERSION.dmg"
-ZIP_PATH="$RELEASE_DIR/AgentAwake-$VERSION.zip"
CHECKSUM_PATH="$RELEASE_DIR/AgentAwake-$VERSION.sha256"
if [[ -z "$SIGNING_IDENTITY" ]]; then
@@ -78,15 +77,6 @@ fi
--wait
/usr/bin/xcrun stapler staple "$DMG_PATH"
/usr/bin/xcrun stapler validate "$DMG_PATH"
-/usr/bin/xcrun stapler staple "$APP_DIR"
-/usr/bin/xcrun stapler validate "$APP_DIR"
-/usr/bin/ditto \
- -c \
- -k \
- --sequesterRsrc \
- --keepParent \
- "$APP_DIR" \
- "$ZIP_PATH"
/usr/sbin/spctl --assess --type execute --verbose=2 "$APP_DIR"
/usr/sbin/spctl \
--assess \
@@ -98,14 +88,10 @@ fi
(
cd "$RELEASE_DIR"
- /usr/bin/shasum -a 256 \
- "${DMG_PATH:t}" \
- "${ZIP_PATH:t}" \
- > "${CHECKSUM_PATH:t}"
+ /usr/bin/shasum -a 256 "${DMG_PATH:t}" > "${CHECKSUM_PATH:t}"
)
/bin/rm -rf "$STAGING_DIR"
echo "$DMG_PATH"
-echo "$ZIP_PATH"
echo "$CHECKSUM_PATH"
From 4dd90102835e99fc878b4c3decc4139ef6e1dc50 Mon Sep 17 00:00:00 2001
From: Raymond
Date: Fri, 31 Jul 2026 23:59:21 +0800
Subject: [PATCH 3/3] Release AgentAwake 0.4.0
---
.github/workflows/ci.yml | 18 +-
.github/workflows/release.yml | 47 +-----
CHANGELOG.md | 23 ++-
README.md | 25 +--
README_EN.md | 32 ++--
Resources/AppIcon.icns | Bin 2075692 -> 2573679 bytes
Resources/AppIcon.png | Bin 1270066 -> 1677157 bytes
Sources/AgentAwake/ProtectionModeSlider.swift | 155 +++++++++++++++---
Sources/AgentAwake/SleepStatusIcon.swift | 54 +++---
Sources/AgentAwake/StatusItemController.swift | 13 +-
Sources/AgentAwake/StatusPopoverView.swift | 6 +-
docs/RELEASING.md | 85 +++-------
scripts/build-app.sh | 36 ++--
scripts/package-release.sh | 125 ++++++--------
14 files changed, 320 insertions(+), 299 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ecd151..e7b95eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,16 +21,12 @@ jobs:
- name: Run self-test
run: swift run --disable-sandbox AgentAwakeSelfTest
- - name: Build Universal 2 app
- run: ./scripts/build-app.sh release universal
+ - name: Build architecture-specific release images
+ run: ./scripts/package-release.sh
- - name: Verify bundle and architectures
+ - name: Verify release images and checksums
run: |
- codesign --verify --deep --strict --verbose=2 dist/AgentAwake.app
- for executable in \
- dist/AgentAwake.app/Contents/MacOS/AgentAwake \
- dist/AgentAwake.app/Contents/Helpers/AgentAwakeHook \
- dist/AgentAwake.app/Contents/Helpers/AgentAwakeHookSetup; do
- lipo -archs "$executable" | grep -q arm64
- lipo -archs "$executable" | grep -q x86_64
- done
+ test -f dist/release/AgentAwake-*-Apple-Silicon.dmg
+ test -f dist/release/AgentAwake-*-Intel.dmg
+ cd dist/release
+ shasum -a 256 -c AgentAwake-*.sha256
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b3d26f1..2436f85 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,52 +24,7 @@ jobs:
Resources/Info.plist)"
test "${GITHUB_REF_NAME#v}" = "$version"
- - name: Import Developer ID certificate
- env:
- DEVELOPER_ID_P12_BASE64: ${{ secrets.DEVELOPER_ID_P12_BASE64 }}
- DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }}
- BUILD_KEYCHAIN_PASSWORD: ${{ secrets.BUILD_KEYCHAIN_PASSWORD }}
- run: |
- certificate_path="$RUNNER_TEMP/developer-id.p12"
- keychain_path="$RUNNER_TEMP/agentawake-signing.keychain-db"
- echo "$DEVELOPER_ID_P12_BASE64" | /usr/bin/base64 --decode \
- > "$certificate_path"
- security create-keychain \
- -p "$BUILD_KEYCHAIN_PASSWORD" "$keychain_path"
- security set-keychain-settings -lut 21600 "$keychain_path"
- security unlock-keychain \
- -p "$BUILD_KEYCHAIN_PASSWORD" "$keychain_path"
- security import "$certificate_path" \
- -P "$DEVELOPER_ID_P12_PASSWORD" \
- -A \
- -t cert \
- -f pkcs12 \
- -k "$keychain_path"
- security list-keychains -d user -s "$keychain_path"
- security set-key-partition-list \
- -S apple-tool:,apple: \
- -s \
- -k "$BUILD_KEYCHAIN_PASSWORD" \
- "$keychain_path"
- security find-identity -v -p codesigning "$keychain_path"
- echo "NOTARY_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
-
- - name: Configure Apple notarization
- env:
- NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
- NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- run: |
- xcrun notarytool store-credentials agentawake-notary \
- --apple-id "$NOTARY_APPLE_ID" \
- --team-id "$APPLE_TEAM_ID" \
- --password "$NOTARY_APP_PASSWORD" \
- --keychain "$NOTARY_KEYCHAIN"
-
- - name: Build, sign, notarize, and package
- env:
- DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
- NOTARY_KEYCHAIN_PROFILE: agentawake-notary
+ - name: Build ad-hoc Apple Silicon and Intel images
run: ./scripts/package-release.sh
- name: Publish GitHub Release
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c4bdab..5bfd543 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,16 +13,19 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
## [Unreleased / 未发布]
+## [0.4.0] - 2026-07-31
+
### Added / 新增
-- 可独立下载的 Universal 2 App 构建,覆盖 Apple Silicon 与 Intel Mac。
- Universal 2 app builds for both Apple silicon and Intel Macs.
+- 分别提供 Apple Silicon 与 Intel Mac 的独立可下载 App 和 DMG。
+ Separate downloadable apps and DMGs for Apple silicon and Intel Macs.
- App 内设置窗口,可选管理登录项、Codex Hooks 与 Claude Hooks。
In-app settings for the login item and optional Codex/Claude Hooks.
- 首次启动自动展示菜单栏面板;定时模式无需任何 Agent 或额外配置。
First launch reveals the menu bar panel; timed modes need no agent or setup.
-- Developer ID 签名、Apple 公证、DMG 制作和 GitHub Release 自动化。
- Developer ID signing, notarization, DMG packaging, and release automation.
+- Ad-hoc 签名、双架构 DMG 制作和无需 Apple 发布凭据的 GitHub Release 自动化。
+ Ad-hoc signing, dual-architecture DMG packaging, and GitHub Release
+ automation without Apple publishing credentials.
### Changed / 变更
@@ -30,6 +33,18 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
本机已检测到的 Agent 配置。
Hooks are now optional settings and only modify explicitly selected,
locally detected agent configurations.
+- 五档滑轨加入悬停光晕、加大的圆形把手、加粗轨道和带触觉反馈的弹性吸附。
+ The five-position slider now has a hover glow, a larger circular thumb,
+ a thicker track, haptic feedback, and spring-loaded snapping.
+- 修复菜单栏面板首次显示时,第一次拖拽只激活窗口而不会移动滑块的问题。
+ Fixed the first drag being consumed by window activation when the menu bar
+ popover is shown for the first time.
+- 菜单栏与 App 内标记统一为四角十字星和单个左倾 `Z`,并将状态栏占位收为方形。
+ The menu bar and in-app marks now share a four-point sparkle with one
+ left-leaning `Z`, while the status item uses a compact square footprint.
+- APP 图标同步换为暖陶土橙配色的四角十字星与单 `Z` 版本。
+ The app icon now uses the same four-point sparkle and single `Z` in a warm
+ clay-orange palette.
## [0.3.1] - 2026-07-31
diff --git a/README.md b/README.md
index 11c3ae7..c3aa0e9 100644
--- a/README.md
+++ b/README.md
@@ -72,19 +72,25 @@ AgentAwake 是一个下载后即可使用的轻量 macOS 菜单栏工具。
前往 [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-1. 下载 `AgentAwake-x.y.z.dmg` 并双击打开;
-2. 在打开的窗口中双击 `AgentAwake.app`,即可直接使用;
-3. 如果准备长期使用,建议将 App 拖入“应用程序”,尤其是在启用“登录时启动”前。
+1. 按本机芯片下载对应文件:
+ - M1、M2、M3、M4 等苹果芯片:`AgentAwake-x.y.z-Apple-Silicon.dmg`;
+ - Intel 芯片:`AgentAwake-x.y.z-Intel.dmg`。
+2. 双击 DMG,在打开的窗口中双击 `AgentAwake.app` 即可使用;如果准备长期使用,
+ 建议先将 App 拖入“应用程序”,尤其是在启用“登录时启动”前。
-拖入“应用程序”不是首次使用的前置条件。首次启动时,macOS 仍会显示一次标准的
-“从互联网下载”安全确认;确认打开后不需要再执行终端命令或手动部署。
+如果不知道芯片类型,点击苹果菜单 →“关于本机”,查看“芯片”或“处理器”。
+
+> **首次打开被 macOS 阻止时:**先尝试双击一次并关闭提示,然后打开
+> “系统设置 → 隐私与安全性”,向下滚动,在 AgentAwake 提示旁点击“仍要打开”,
+> 再确认“打开”(可能需要输入登录密码)。这只需操作一次,以后可以直接双击。
+> 本版本采用 ad-hoc 签名,因此不会显示已验证的 Apple 开发者身份;不需要终端命令。
首次启动后,AgentAwake 会自动展开一次菜单栏面板,之后只保留菜单栏图标,
不显示 Dock 图标。三个定时模式此时已经可以使用,不需要继续配置。
### 2. 选择保护模式
-1. 点击菜单栏中的星星与三个 `Z` 图标。
+1. 点击菜单栏中的四角十字星与单个 `Z` 图标。
2. 拖动滑块选择 30 分钟、1 小时、2 小时或 Agent。
3. 需要立即停止时,把滑块拖回“未开启”;临时断言会马上释放。
@@ -162,11 +168,10 @@ app-server、sandbox 和流式进程可能长期常驻。
从源码构建需要 Swift 工具链;安装 Xcode Command Line Tools 即可。
-构建 Universal 2 App:
+构建 Apple Silicon 与 Intel 发布镜像:
```bash
-./scripts/build-app.sh release universal
-open dist/AgentAwake.app
+./scripts/package-release.sh
```
运行零依赖自检:
@@ -193,7 +198,7 @@ Sources/AgentAwakeHook/ 单次生命周期 Hook helper
Sources/AgentAwakeHookSetup/ Hooks 安装与卸载
Resources/ Info.plist 与 APP 图标
scripts/ App 和图标构建脚本
-docs/RELEASING.md 正式签名、公证与 GitHub Release 流程
+docs/RELEASING.md 双架构 DMG 与 GitHub Release 流程
```
## 当前边界
diff --git a/README_EN.md b/README_EN.md
index 647fd72..f47ee71 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -77,14 +77,21 @@ or keeps a persistent `caffeinate` or shell process running.
Open [GitHub Releases](https://github.com/Raymond711xl/AgentAwake/releases/latest):
-1. Download and open `AgentAwake-x.y.z.dmg`.
-2. Double-click `AgentAwake.app` in the mounted window to use it immediately.
-3. For long-term use, drag the app to Applications, especially before enabling
- Launch at Login.
-
-Moving the app to Applications is not required for the first run. macOS still
-shows its standard downloaded-from-the-internet confirmation once; after you
-confirm, no Terminal commands or manual deployment are needed.
+1. Download the image for your Mac:
+ - Apple M1, M2, M3, M4, or later: `AgentAwake-x.y.z-Apple-Silicon.dmg`.
+ - Intel processor: `AgentAwake-x.y.z-Intel.dmg`.
+2. Open the DMG and double-click `AgentAwake.app`. For long-term use, drag the
+ app to Applications first, especially before enabling Launch at Login.
+
+To check your processor, open Apple menu → About This Mac and look for Chip
+or Processor.
+
+> **If macOS blocks the first launch:** try opening AgentAwake once and dismiss
+> the warning. Open System Settings → Privacy & Security, scroll down, click
+> Open Anyway beside AgentAwake, then confirm Open. macOS may ask for your login
+> password. This is a one-time exception; future launches work normally.
+> This release uses ad-hoc signing and does not show a verified Apple developer.
+> No Terminal command is required.
On its first launch, AgentAwake opens its menu bar panel once. It then remains
in the menu bar without a Dock icon. All three timed modes are immediately
@@ -92,7 +99,7 @@ available and require no further setup.
### 2. Choose a protection mode
-1. Click the star-and-three-`Z` icon in the menu bar.
+1. Click the four-point sparkle-and-`Z` icon in the menu bar.
2. Drag the slider to 30 minutes, 1 hour, 2 hours, or Agent.
3. To stop immediately, drag it back to Off (`未开启`); the temporary power
assertions are released at once.
@@ -181,11 +188,10 @@ automatically.
Source builds require a Swift toolchain; Xcode Command Line Tools are
sufficient.
-Build the Universal 2 app:
+Build the Apple silicon and Intel release images:
```bash
-./scripts/build-app.sh release universal
-open dist/AgentAwake.app
+./scripts/package-release.sh
```
Run the dependency-free self-test:
@@ -212,7 +218,7 @@ Sources/AgentAwakeHook/ One-shot lifecycle Hook helper
Sources/AgentAwakeHookSetup/ Hook installer and uninstaller
Resources/ Info.plist and app icon
scripts/ App and icon build scripts
-docs/RELEASING.md Signing, notarization, and GitHub Release workflow
+docs/RELEASING.md Dual-architecture DMG and GitHub Release workflow
```
## Current scope
diff --git a/Resources/AppIcon.icns b/Resources/AppIcon.icns
index 98752e979c3fb5bab575223a793cda6adbc6f4c4..50f62e0d1d48268c7397fc9e1a9ccbae7c743ada 100644
GIT binary patch
literal 2573679
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