Vibecode Alfred Powerpack
track this build5 phases, 20 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Alfred Powerpack, build a local launcher with search, snippets, clipboard history, and workflows. The hard boundary is years of macos integration, workflow ecosystem, reliability, and community, plus os integration polish and maintenance.
You are building a lean indie version of Alfred Powerpack. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Alfred Powerpack · indie build A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI, built in Xcode | the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them | | App type | A background agent with LSUIElement set to true | a launcher with a Dock icon and a menu bar is wrong twice over | | Hotkey | Carbon RegisterEventHotKey | the one API that gets a system-wide hotkey without the Accessibility grant | | Storage | SQLite through the C API in a container directory | history and usage counts are rows; a plist rewritten on every keystroke is not | | Snippet delivery | NSPasteboard plus a synthetic Cmd-V | per-character CGEvent posting drops characters and mangles anything non-ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you already own a Mac - Why: The whole build is AppKit. There is no way to develop or test this on another platform. - Get it: Any Mac from the last few years running Sonoma or later. Check with the Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds the app, signs it, and provides the Instruments profiler you will want when the keystroke monitor feels slow. - Get it: Install from the Mac App Store, or download from developer.apple.com/xcode. It is a large download, so start it before you read further. - Verify: xcodebuild -version prints 16 or higher - [ ] **Comfort with Swift and a little AppKit** · free - Why: SwiftUI covers the panel, but hotkeys, the pasteboard and event monitors are all AppKit and Carbon. You will be reading Apple documentation, not tutorials. - Get it: If you have never written Swift, build the panel in Phase 1 by itself and stop there for a day before continuing. - [ ] **Willingness to grant Accessibility to your own debug build** · free - Why: Snippets and paste-back cannot work without it. You are granting a program you compiled the ability to read keystrokes, so understand what that means before you turn it on. - Get it: System Settings > Privacy and Security > Accessibility, then add your built app. During development this must be re-granted whenever the binary path changes. - Verify: AXIsProcessTrusted() returns true from the running app - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Only needed for the product phase. Without it you cannot get a Developer ID certificate, so you cannot notarize, so the app shows a Gatekeeper warning on anyone else's Mac. - Get it: Enrol at developer.apple.com/programs. Enrolment is reviewed and can take a day or two. - Verify: A Developer ID Application certificate appears in Xcode > Settings > Accounts ## Quick start ```sh codesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Launcher.app ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. - years of macOS integration, workflow ecosystem, reliability, and community - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Alfred Powerpack, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Alfred Powerpack The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first launcher for macOS with app search, clipboard history, snippets and script workflows to replace Alfred Powerpack. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI, built in Xcode · the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them - App type: A background agent with LSUIElement set to true · a launcher with a Dock icon and a menu bar is wrong twice over - Hotkey: Carbon RegisterEventHotKey · the one API that gets a system-wide hotkey without the Accessibility grant - Storage: SQLite through the C API in a container directory · history and usage counts are rows; a plist rewritten on every keystroke is not - Snippet delivery: NSPasteboard plus a synthetic Cmd-V · per-character CGEvent posting drops characters and mangles anything non-ASCII ### Data model (create this before Phase 1) - items(id, kind, title, subtitle, path, bundle_id, indexed_at) · kind is app, file or workflow_result - usage(item_id, used_at, query_prefix) · one row per launch, the input to frecency - clips(id, kind, content, preview, source_bundle_id, hash, copied_at, pinned) - snippets(id, trigger, body, expand_count, last_used_at) - workflows(id, name, keyword, script_path, timeout_ms, enabled) - Index items on (kind, title) and clips on (copied_at desc); the query runs on every keystroke. ### Phase 1 · Agent, hotkey and window Build: An Xcode app target with LSUIElement true in Info.plist, a menu bar item, and a borderless floating NSPanel that becomes key without stealing focus permanently. Register a single global hotkey with RegisterEventHotKey and make it configurable. The panel shows a text field and an empty result list. Nothing searches yet. Trap: RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. Done when: The hotkey opens the panel while a full-screen app is frontmost, Escape closes it and returns focus to that same app, and the process has no Dock icon and no window in the app switcher. Do not build yet: No searching, no clipboard capture, no scripts. A window that appears and disappears reliably is the whole phase. ### Phase 2 · App search with frecency Build: Scan /Applications, /System/Applications and ~/Applications for bundles, read CFBundleDisplayName and the icon, and store them in items. Fuzzy match the query against title and an acronym of its words. Rank by match quality multiplied by a frecency score built from the usage table: recent launches weigh more than old ones, and a launch after the same query prefix weighs most of all. Trap: Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. Done when: Typing saf ranks Safari first. Launching a lower-ranked result twice for the same prefix promotes it above the previous winner. A newly installed app appears without a relaunch, within one rescan interval. Do not build yet: No file search, no clipboard, no scripts. Applications only. ### Phase 3 · Clipboard history Build: Poll NSPasteboard.general.changeCount on a timer and capture new contents into clips. Store text and image data with a hash so a repeated copy updates a timestamp rather than adding a duplicate row. Add a second hotkey that opens the panel in history mode, and pasting a row puts it back on the pasteboard and sends Cmd-V. Trap: NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. Trap: Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. Done when: Copying three things and opening history shows three rows, newest first. Copying the same string twice leaves one row with an updated timestamp. A password copied from a password manager does not appear in the history at all. Do not build yet: No snippets and no scripts. Capture, search and paste back. ### Phase 4 · Snippets Build: Watch for trigger strings with a global key event monitor, delete the trigger with the right number of backspaces, then put the body on the pasteboard and send Cmd-V. Restore the user's previous pasteboard contents afterwards. Trap: Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. Trap: Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. Done when: Typing a configured trigger in a plain text editor replaces it with the body, including an emoji and an accented character, with no characters lost. The same trigger in a password field leaves the field untouched and surfaces a visible refusal. The pasteboard afterwards holds what it held before the expansion. Do not build yet: No script workflows yet. ### Phase 5 · Script workflows Build: A workflow is a keyword plus an executable. When the query starts with the keyword, run the script with the rest of the query as its single argument, read JSON lines from stdout, and render each as a selectable row. Selecting a row runs the script again with the row's action payload. Trap: Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. Done when: A script printing two JSON lines shows two selectable rows. A script that sleeps past its timeout is killed and shows a timeout row while the launcher stays responsive to typing. A script exiting non-zero shows its stderr in an error row rather than showing nothing. Do not build yet: No signing or distribution yet. ### Phase 6 · Signing, notarization and updates Build: Enable the hardened runtime, sign with a Developer ID certificate, notarize with notarytool, staple the ticket, and ship updates through Sparkle. Add launch at login with SMAppService. Trap: Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. Done when: A signed and notarized build downloaded on a second Mac opens with no Gatekeeper warning, spctl reports it accepted, and an update served from your appcast installs and relaunches. Do not build yet: Nothing after this. This is the last phase. ### Out of scope (and why) - Cross-device sync. Alfred does not sync either, and building it means a server, accounts and conflict resolution, which is a different product. - The workflow gallery. What you are paying for in the Powerpack is fifteen years of other people's workflows, and no amount of code produces that. - Deep macOS integrations like Contacts, Music control and 1Password lookup. Each is a small piece of work and there are forty of them. - The fifteen years of OS version edge cases behind Alfred's reliability. Every macOS release moves something, and you now own that maintenance. ### README must contain - Which permissions are needed, which feature each unlocks, and the fact that Accessibility and Input Monitoring are separate grants - That clipboard history skips concealed pasteboard types, and what that means for password managers - The workflow JSON line contract with a copyable example - Where the SQLite database lives and how to back it up - That re-signing the app invalidates existing permission grants and how to fix that ===== AGENTS.md ===== # Agent instructions · Alfred Powerpack indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI, built in Xcode, A background agent with LSUIElement set to true, Carbon RegisterEventHotKey, SQLite through the C API in a container directory, NSPasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ===== BUILD_PLAN.md ===== # Build plan · Alfred Powerpack A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Agent, hotkey and window A background app with no Dock icon that appears on a global hotkey from inside any other application and disappears on Escape. ### Steps 1. Create the Xcode app target and hide it from the Dock File > New > Project > macOS > App, SwiftUI lifecycle. Add the key Application is agent (UIElement) with value YES to Info.plist. Run it: nothing should appear in the Dock. Files: `Info.plist` 2. Add a menu bar item with Quit and Preferences Use MenuBarExtra in SwiftUI. This is the only visible surface until the hotkey works, and it is how you quit a Dock-less app you cannot click. Files: `App.swift` 3. Build the panel as a borderless NSPanel Subclass NSPanel with .nonactivatingPanel and .hudWindow, set canBecomeKey to true, level to .floating and hidesOnDeactivate to true. A plain NSWindow cannot take keyboard input without activating the app. Files: `LauncherPanel.swift` 4. Register the global hotkey Call RegisterEventHotKey with a signature and an EventHotKeyID, and install a Carbon event handler for kEventHotKeyPressed. Store the chosen key combination in UserDefaults so it survives a relaunch. Files: `Hotkey.swift` ### Done when - [ ] The hotkey opens the panel while a different app is frontmost and full screen - [ ] Escape closes the panel and keyboard focus returns to the app that was frontmost - [ ] The app shows no Dock icon and does not appear in the Cmd-Tab switcher - [ ] Quitting from the menu bar item and relaunching restores the configured hotkey ### Watch out - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. ## Phase 2 · App search with frecency Typing two or three letters puts the app you actually meant at the top, and it gets better with use. ### Steps 1. Enumerate application bundles Read the three application directories with FileManager, take every .app bundle, and pull CFBundleDisplayName or CFBundleName plus the bundle identifier from its Info.plist. Store one row per app in items. Files: `Indexer.swift` 2. Write the fuzzy matcher Score a subsequence match: contiguous runs score higher, a match at a word boundary scores higher, and build an acronym from capitalised words so ahk finds Adobe Handbrake Kit. Return a score, not a boolean. Files: `Match.swift` 3. Add the frecency multiplier On every launch insert into usage with the query prefix that produced it. Score an item as matchScore multiplied by (1 plus a decayed sum over its usage rows), halving each row's weight every fourteen days. Files: `Ranking.swift` 4. Rescan on a timer and on application directory changes Rescan every few minutes, and watch the application directories with an FSEvents stream so an install shows up without waiting. Files: `Indexer.swift` ### Done when - [ ] Typing saf puts Safari first - [ ] Launching the second result for the same prefix twice moves it to first place - [ ] An app installed while the launcher is running appears without a relaunch - [ ] A query matching nothing shows an empty state, not the previous results ### Watch out - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. ## Phase 3 · Clipboard history Everything you copy is searchable later, except the things that must never be stored. ### Steps 1. Poll changeCount on a timer A repeating timer at roughly 200ms compares NSPasteboard.general.changeCount to the last value you saw. Only read the pasteboard when the count moved. Files: `ClipboardWatcher.swift` 2. Refuse concealed and transient items Before reading contents, inspect the item types. Skip anything declaring org.nspasteboard.ConcealedType, org.nspasteboard.TransientType or org.nspasteboard.AutoGeneratedType. Write a test that feeds a synthetic concealed item and asserts nothing was stored. Files: `ClipboardWatcher.swift` 3. Store with a content hash Hash the text or image bytes. On a hash you already hold, update copied_at instead of inserting. Cap history by row count and by total bytes, and delete oldest first, never pinned rows. Files: `ClipStore.swift` 4. Add history mode and paste back A second registered hotkey opens the panel filtered to clips. Selecting a row writes it to the pasteboard and posts Cmd-V. This is where the Accessibility grant is first needed, so explain it in the prompt you show. Files: `Paste.swift` ### Done when - [ ] Three copies produce three rows, newest first - [ ] Copying the same text twice leaves one row with a moved timestamp - [ ] A password copied out of a password manager is absent from the history - [ ] Selecting a history row pastes it into the previously focused app ### Watch out - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. ## Phase 4 · Snippets A trigger you type anywhere expands into a block of text, without dropping characters. ### Steps 1. Add a global key monitor behind the Accessibility grant NSEvent.addGlobalMonitorForEvents for keyDown requires Accessibility. Check AXIsProcessTrustedWithOptions and show your own explanation before the system prompt appears. Files: `SnippetWatcher.swift` 2. Match triggers against a rolling buffer Keep a short rolling buffer of recent characters and reset it on Return, Escape, a click or a focus change. Match the longest trigger that ends at the buffer tail. Files: `SnippetWatcher.swift` 3. Expand through the pasteboard and restore Save the current pasteboard items, send backspaces for the trigger length, write the body, send Cmd-V, then restore the saved items after a short delay. Restoring too early pastes the wrong thing. Files: `Expand.swift` 4. Detect and report a refused expansion If the frontmost element is a secure text field, do not attempt the expansion. Show a brief notification saying why. Files: `Expand.swift` ### Done when - [ ] A trigger expands in TextEdit with an emoji and an accented character intact - [ ] The pasteboard after an expansion holds what it held before - [ ] A trigger typed into a password field leaves the field untouched and shows a refusal - [ ] Expansion count increments on the snippet row ### Watch out - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. ## Phase 5 · Script workflows Your own scripts become result providers, so the launcher does things this build never anticipated. ### Steps 1. Define the JSON line contract and document it One JSON object per line with title, subtitle, an optional icon path and an arg passed back on selection. Write the contract in the README before implementing it. Files: `docs/workflows.md` 2. Run scripts with a timeout and cancellation Use Process with a dispatch queue. Cancel and kill the previous run when a new keystroke arrives, so an abandoned query never outlives its input. Files: `WorkflowRunner.swift` 3. Render results and handle selection Map JSON lines to rows. On selection re-invoke the script with the arg and a mode flag so it can act rather than list. Files: `WorkflowRunner.swift` 4. Ship two example workflows A directory search using mdfind and a unit converter. They prove the contract and give the user something to copy. Files: `examples/` ### Done when - [ ] A script printing two JSON lines renders two rows - [ ] A script that sleeps past the timeout is killed and the launcher keeps accepting input - [ ] A non-zero exit shows an error row carrying the stderr text - [ ] Typing quickly cancels the previous run rather than queueing runs ### Watch out - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ## Not in this build - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. ## After v1, if you want it - File search with NSMetadataQuery, once you accept the permission prompts it brings - A calculator result row using NSExpression - Sharing workflows as a signed folder others can drop in ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. LAUNCHER_DB_PATH=~/Library/Application Support/Launcher/launcher.db # Required. How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. LAUNCHER_CLIP_LIMIT=500 # Required. How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. LAUNCHER_WORKFLOW_TIMEOUT_MS=1500 # Optional. Product phase only. The HTTPS URL where you publish your Sparkle appcast. SPARKLE_FEED_URL=https://example.com/appcast.xml
You are building a lean indie version of Alfred Powerpack. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Alfred Powerpack · indie build A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI, built in Xcode | the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them | | App type | A background agent with LSUIElement set to true | a launcher with a Dock icon and a menu bar is wrong twice over | | Hotkey | Carbon RegisterEventHotKey | the one API that gets a system-wide hotkey without the Accessibility grant | | Storage | SQLite through the C API in a container directory | history and usage counts are rows; a plist rewritten on every keystroke is not | | Snippet delivery | NSPasteboard plus a synthetic Cmd-V | per-character CGEvent posting drops characters and mangles anything non-ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you already own a Mac - Why: The whole build is AppKit. There is no way to develop or test this on another platform. - Get it: Any Mac from the last few years running Sonoma or later. Check with the Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds the app, signs it, and provides the Instruments profiler you will want when the keystroke monitor feels slow. - Get it: Install from the Mac App Store, or download from developer.apple.com/xcode. It is a large download, so start it before you read further. - Verify: xcodebuild -version prints 16 or higher - [ ] **Comfort with Swift and a little AppKit** · free - Why: SwiftUI covers the panel, but hotkeys, the pasteboard and event monitors are all AppKit and Carbon. You will be reading Apple documentation, not tutorials. - Get it: If you have never written Swift, build the panel in Phase 1 by itself and stop there for a day before continuing. - [ ] **Willingness to grant Accessibility to your own debug build** · free - Why: Snippets and paste-back cannot work without it. You are granting a program you compiled the ability to read keystrokes, so understand what that means before you turn it on. - Get it: System Settings > Privacy and Security > Accessibility, then add your built app. During development this must be re-granted whenever the binary path changes. - Verify: AXIsProcessTrusted() returns true from the running app - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Only needed for the product phase. Without it you cannot get a Developer ID certificate, so you cannot notarize, so the app shows a Gatekeeper warning on anyone else's Mac. - Get it: Enrol at developer.apple.com/programs. Enrolment is reviewed and can take a day or two. - Verify: A Developer ID Application certificate appears in Xcode > Settings > Accounts ## Quick start ```sh codesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Launcher.app ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. - years of macOS integration, workflow ecosystem, reliability, and community - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Alfred Powerpack, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Alfred Powerpack The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first launcher for macOS with app search, clipboard history, snippets and script workflows to replace Alfred Powerpack. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI, built in Xcode · the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them - App type: A background agent with LSUIElement set to true · a launcher with a Dock icon and a menu bar is wrong twice over - Hotkey: Carbon RegisterEventHotKey · the one API that gets a system-wide hotkey without the Accessibility grant - Storage: SQLite through the C API in a container directory · history and usage counts are rows; a plist rewritten on every keystroke is not - Snippet delivery: NSPasteboard plus a synthetic Cmd-V · per-character CGEvent posting drops characters and mangles anything non-ASCII ### Data model (create this before Phase 1) - items(id, kind, title, subtitle, path, bundle_id, indexed_at) · kind is app, file or workflow_result - usage(item_id, used_at, query_prefix) · one row per launch, the input to frecency - clips(id, kind, content, preview, source_bundle_id, hash, copied_at, pinned) - snippets(id, trigger, body, expand_count, last_used_at) - workflows(id, name, keyword, script_path, timeout_ms, enabled) - Index items on (kind, title) and clips on (copied_at desc); the query runs on every keystroke. ### Phase 1 · Agent, hotkey and window Build: An Xcode app target with LSUIElement true in Info.plist, a menu bar item, and a borderless floating NSPanel that becomes key without stealing focus permanently. Register a single global hotkey with RegisterEventHotKey and make it configurable. The panel shows a text field and an empty result list. Nothing searches yet. Trap: RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. Done when: The hotkey opens the panel while a full-screen app is frontmost, Escape closes it and returns focus to that same app, and the process has no Dock icon and no window in the app switcher. Do not build yet: No searching, no clipboard capture, no scripts. A window that appears and disappears reliably is the whole phase. ### Phase 2 · App search with frecency Build: Scan /Applications, /System/Applications and ~/Applications for bundles, read CFBundleDisplayName and the icon, and store them in items. Fuzzy match the query against title and an acronym of its words. Rank by match quality multiplied by a frecency score built from the usage table: recent launches weigh more than old ones, and a launch after the same query prefix weighs most of all. Trap: Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. Done when: Typing saf ranks Safari first. Launching a lower-ranked result twice for the same prefix promotes it above the previous winner. A newly installed app appears without a relaunch, within one rescan interval. Do not build yet: No file search, no clipboard, no scripts. Applications only. ### Phase 3 · Clipboard history Build: Poll NSPasteboard.general.changeCount on a timer and capture new contents into clips. Store text and image data with a hash so a repeated copy updates a timestamp rather than adding a duplicate row. Add a second hotkey that opens the panel in history mode, and pasting a row puts it back on the pasteboard and sends Cmd-V. Trap: NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. Trap: Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. Done when: Copying three things and opening history shows three rows, newest first. Copying the same string twice leaves one row with an updated timestamp. A password copied from a password manager does not appear in the history at all. Do not build yet: No snippets and no scripts. Capture, search and paste back. ### Phase 4 · Snippets Build: Watch for trigger strings with a global key event monitor, delete the trigger with the right number of backspaces, then put the body on the pasteboard and send Cmd-V. Restore the user's previous pasteboard contents afterwards. Trap: Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. Trap: Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. Done when: Typing a configured trigger in a plain text editor replaces it with the body, including an emoji and an accented character, with no characters lost. The same trigger in a password field leaves the field untouched and surfaces a visible refusal. The pasteboard afterwards holds what it held before the expansion. Do not build yet: No script workflows yet. ### Phase 5 · Script workflows Build: A workflow is a keyword plus an executable. When the query starts with the keyword, run the script with the rest of the query as its single argument, read JSON lines from stdout, and render each as a selectable row. Selecting a row runs the script again with the row's action payload. Trap: Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. Done when: A script printing two JSON lines shows two selectable rows. A script that sleeps past its timeout is killed and shows a timeout row while the launcher stays responsive to typing. A script exiting non-zero shows its stderr in an error row rather than showing nothing. Do not build yet: No signing or distribution yet. ### Phase 6 · Signing, notarization and updates Build: Enable the hardened runtime, sign with a Developer ID certificate, notarize with notarytool, staple the ticket, and ship updates through Sparkle. Add launch at login with SMAppService. Trap: Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. Done when: A signed and notarized build downloaded on a second Mac opens with no Gatekeeper warning, spctl reports it accepted, and an update served from your appcast installs and relaunches. Do not build yet: Nothing after this. This is the last phase. ### Out of scope (and why) - Cross-device sync. Alfred does not sync either, and building it means a server, accounts and conflict resolution, which is a different product. - The workflow gallery. What you are paying for in the Powerpack is fifteen years of other people's workflows, and no amount of code produces that. - Deep macOS integrations like Contacts, Music control and 1Password lookup. Each is a small piece of work and there are forty of them. - The fifteen years of OS version edge cases behind Alfred's reliability. Every macOS release moves something, and you now own that maintenance. ### README must contain - Which permissions are needed, which feature each unlocks, and the fact that Accessibility and Input Monitoring are separate grants - That clipboard history skips concealed pasteboard types, and what that means for password managers - The workflow JSON line contract with a copyable example - Where the SQLite database lives and how to back it up - That re-signing the app invalidates existing permission grants and how to fix that ===== AGENTS.md ===== # Agent instructions · Alfred Powerpack indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI, built in Xcode, A background agent with LSUIElement set to true, Carbon RegisterEventHotKey, SQLite through the C API in a container directory, NSPasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ===== BUILD_PLAN.md ===== # Build plan · Alfred Powerpack A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Agent, hotkey and window A background app with no Dock icon that appears on a global hotkey from inside any other application and disappears on Escape. ### Steps 1. Create the Xcode app target and hide it from the Dock File > New > Project > macOS > App, SwiftUI lifecycle. Add the key Application is agent (UIElement) with value YES to Info.plist. Run it: nothing should appear in the Dock. Files: `Info.plist` 2. Add a menu bar item with Quit and Preferences Use MenuBarExtra in SwiftUI. This is the only visible surface until the hotkey works, and it is how you quit a Dock-less app you cannot click. Files: `App.swift` 3. Build the panel as a borderless NSPanel Subclass NSPanel with .nonactivatingPanel and .hudWindow, set canBecomeKey to true, level to .floating and hidesOnDeactivate to true. A plain NSWindow cannot take keyboard input without activating the app. Files: `LauncherPanel.swift` 4. Register the global hotkey Call RegisterEventHotKey with a signature and an EventHotKeyID, and install a Carbon event handler for kEventHotKeyPressed. Store the chosen key combination in UserDefaults so it survives a relaunch. Files: `Hotkey.swift` ### Done when - [ ] The hotkey opens the panel while a different app is frontmost and full screen - [ ] Escape closes the panel and keyboard focus returns to the app that was frontmost - [ ] The app shows no Dock icon and does not appear in the Cmd-Tab switcher - [ ] Quitting from the menu bar item and relaunching restores the configured hotkey ### Watch out - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. ## Phase 2 · App search with frecency Typing two or three letters puts the app you actually meant at the top, and it gets better with use. ### Steps 1. Enumerate application bundles Read the three application directories with FileManager, take every .app bundle, and pull CFBundleDisplayName or CFBundleName plus the bundle identifier from its Info.plist. Store one row per app in items. Files: `Indexer.swift` 2. Write the fuzzy matcher Score a subsequence match: contiguous runs score higher, a match at a word boundary scores higher, and build an acronym from capitalised words so ahk finds Adobe Handbrake Kit. Return a score, not a boolean. Files: `Match.swift` 3. Add the frecency multiplier On every launch insert into usage with the query prefix that produced it. Score an item as matchScore multiplied by (1 plus a decayed sum over its usage rows), halving each row's weight every fourteen days. Files: `Ranking.swift` 4. Rescan on a timer and on application directory changes Rescan every few minutes, and watch the application directories with an FSEvents stream so an install shows up without waiting. Files: `Indexer.swift` ### Done when - [ ] Typing saf puts Safari first - [ ] Launching the second result for the same prefix twice moves it to first place - [ ] An app installed while the launcher is running appears without a relaunch - [ ] A query matching nothing shows an empty state, not the previous results ### Watch out - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. ## Phase 3 · Clipboard history Everything you copy is searchable later, except the things that must never be stored. ### Steps 1. Poll changeCount on a timer A repeating timer at roughly 200ms compares NSPasteboard.general.changeCount to the last value you saw. Only read the pasteboard when the count moved. Files: `ClipboardWatcher.swift` 2. Refuse concealed and transient items Before reading contents, inspect the item types. Skip anything declaring org.nspasteboard.ConcealedType, org.nspasteboard.TransientType or org.nspasteboard.AutoGeneratedType. Write a test that feeds a synthetic concealed item and asserts nothing was stored. Files: `ClipboardWatcher.swift` 3. Store with a content hash Hash the text or image bytes. On a hash you already hold, update copied_at instead of inserting. Cap history by row count and by total bytes, and delete oldest first, never pinned rows. Files: `ClipStore.swift` 4. Add history mode and paste back A second registered hotkey opens the panel filtered to clips. Selecting a row writes it to the pasteboard and posts Cmd-V. This is where the Accessibility grant is first needed, so explain it in the prompt you show. Files: `Paste.swift` ### Done when - [ ] Three copies produce three rows, newest first - [ ] Copying the same text twice leaves one row with a moved timestamp - [ ] A password copied out of a password manager is absent from the history - [ ] Selecting a history row pastes it into the previously focused app ### Watch out - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. ## Phase 4 · Snippets A trigger you type anywhere expands into a block of text, without dropping characters. ### Steps 1. Add a global key monitor behind the Accessibility grant NSEvent.addGlobalMonitorForEvents for keyDown requires Accessibility. Check AXIsProcessTrustedWithOptions and show your own explanation before the system prompt appears. Files: `SnippetWatcher.swift` 2. Match triggers against a rolling buffer Keep a short rolling buffer of recent characters and reset it on Return, Escape, a click or a focus change. Match the longest trigger that ends at the buffer tail. Files: `SnippetWatcher.swift` 3. Expand through the pasteboard and restore Save the current pasteboard items, send backspaces for the trigger length, write the body, send Cmd-V, then restore the saved items after a short delay. Restoring too early pastes the wrong thing. Files: `Expand.swift` 4. Detect and report a refused expansion If the frontmost element is a secure text field, do not attempt the expansion. Show a brief notification saying why. Files: `Expand.swift` ### Done when - [ ] A trigger expands in TextEdit with an emoji and an accented character intact - [ ] The pasteboard after an expansion holds what it held before - [ ] A trigger typed into a password field leaves the field untouched and shows a refusal - [ ] Expansion count increments on the snippet row ### Watch out - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. ## Phase 5 · Script workflows Your own scripts become result providers, so the launcher does things this build never anticipated. ### Steps 1. Define the JSON line contract and document it One JSON object per line with title, subtitle, an optional icon path and an arg passed back on selection. Write the contract in the README before implementing it. Files: `docs/workflows.md` 2. Run scripts with a timeout and cancellation Use Process with a dispatch queue. Cancel and kill the previous run when a new keystroke arrives, so an abandoned query never outlives its input. Files: `WorkflowRunner.swift` 3. Render results and handle selection Map JSON lines to rows. On selection re-invoke the script with the arg and a mode flag so it can act rather than list. Files: `WorkflowRunner.swift` 4. Ship two example workflows A directory search using mdfind and a unit converter. They prove the contract and give the user something to copy. Files: `examples/` ### Done when - [ ] A script printing two JSON lines renders two rows - [ ] A script that sleeps past the timeout is killed and the launcher keeps accepting input - [ ] A non-zero exit shows an error row carrying the stderr text - [ ] Typing quickly cancels the previous run rather than queueing runs ### Watch out - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ## Not in this build - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. ## After v1, if you want it - File search with NSMetadataQuery, once you accept the permission prompts it brings - A calculator result row using NSExpression - Sharing workflows as a signed folder others can drop in ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. LAUNCHER_DB_PATH=~/Library/Application Support/Launcher/launcher.db # Required. How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. LAUNCHER_CLIP_LIMIT=500 # Required. How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. LAUNCHER_WORKFLOW_TIMEOUT_MS=1500 # Optional. Product phase only. The HTTPS URL where you publish your Sparkle appcast. SPARKLE_FEED_URL=https://example.com/appcast.xml
You are building a production product version of Alfred Powerpack. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Alfred Powerpack · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Alfred Powerpack, build a local launcher with search, snippets, clipboard history, and workflows. The hard boundary is years of macos integration, workflow ecosystem, reliability, and community, plus os integration polish and maintenance. ## Product outcome A signed, notarized launcher that other people can install, that updates itself, and whose permission requirements are documented well enough that a stranger grants them knowingly. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. - years of macOS integration, workflow ecosystem, reliability, and community - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - spctl accepts the notarized build and it opens clean on a Mac that never had it installed - A concealed pasteboard item is still refused by the test written in Phase 3 - Snippet expansion is verified in at least three different text engines, including a browser - Instruments shows idle CPU under one percent with the clipboard watcher running - The README states every permission and what refusing it costs ===== BRIEF.md ===== # Build brief · Alfred Powerpack The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first launcher for macOS with app search, clipboard history, snippets and script workflows to replace Alfred Powerpack. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI, built in Xcode · the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them - App type: A background agent with LSUIElement set to true · a launcher with a Dock icon and a menu bar is wrong twice over - Hotkey: Carbon RegisterEventHotKey · the one API that gets a system-wide hotkey without the Accessibility grant - Storage: SQLite through the C API in a container directory · history and usage counts are rows; a plist rewritten on every keystroke is not - Snippet delivery: NSPasteboard plus a synthetic Cmd-V · per-character CGEvent posting drops characters and mangles anything non-ASCII ### Data model (create this before Phase 1) - items(id, kind, title, subtitle, path, bundle_id, indexed_at) · kind is app, file or workflow_result - usage(item_id, used_at, query_prefix) · one row per launch, the input to frecency - clips(id, kind, content, preview, source_bundle_id, hash, copied_at, pinned) - snippets(id, trigger, body, expand_count, last_used_at) - workflows(id, name, keyword, script_path, timeout_ms, enabled) - Index items on (kind, title) and clips on (copied_at desc); the query runs on every keystroke. ### Phase 1 · Agent, hotkey and window Build: An Xcode app target with LSUIElement true in Info.plist, a menu bar item, and a borderless floating NSPanel that becomes key without stealing focus permanently. Register a single global hotkey with RegisterEventHotKey and make it configurable. The panel shows a text field and an empty result list. Nothing searches yet. Trap: RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. Done when: The hotkey opens the panel while a full-screen app is frontmost, Escape closes it and returns focus to that same app, and the process has no Dock icon and no window in the app switcher. Do not build yet: No searching, no clipboard capture, no scripts. A window that appears and disappears reliably is the whole phase. ### Phase 2 · App search with frecency Build: Scan /Applications, /System/Applications and ~/Applications for bundles, read CFBundleDisplayName and the icon, and store them in items. Fuzzy match the query against title and an acronym of its words. Rank by match quality multiplied by a frecency score built from the usage table: recent launches weigh more than old ones, and a launch after the same query prefix weighs most of all. Trap: Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. Done when: Typing saf ranks Safari first. Launching a lower-ranked result twice for the same prefix promotes it above the previous winner. A newly installed app appears without a relaunch, within one rescan interval. Do not build yet: No file search, no clipboard, no scripts. Applications only. ### Phase 3 · Clipboard history Build: Poll NSPasteboard.general.changeCount on a timer and capture new contents into clips. Store text and image data with a hash so a repeated copy updates a timestamp rather than adding a duplicate row. Add a second hotkey that opens the panel in history mode, and pasting a row puts it back on the pasteboard and sends Cmd-V. Trap: NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. Trap: Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. Done when: Copying three things and opening history shows three rows, newest first. Copying the same string twice leaves one row with an updated timestamp. A password copied from a password manager does not appear in the history at all. Do not build yet: No snippets and no scripts. Capture, search and paste back. ### Phase 4 · Snippets Build: Watch for trigger strings with a global key event monitor, delete the trigger with the right number of backspaces, then put the body on the pasteboard and send Cmd-V. Restore the user's previous pasteboard contents afterwards. Trap: Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. Trap: Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. Done when: Typing a configured trigger in a plain text editor replaces it with the body, including an emoji and an accented character, with no characters lost. The same trigger in a password field leaves the field untouched and surfaces a visible refusal. The pasteboard afterwards holds what it held before the expansion. Do not build yet: No script workflows yet. ### Phase 5 · Script workflows Build: A workflow is a keyword plus an executable. When the query starts with the keyword, run the script with the rest of the query as its single argument, read JSON lines from stdout, and render each as a selectable row. Selecting a row runs the script again with the row's action payload. Trap: Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. Done when: A script printing two JSON lines shows two selectable rows. A script that sleeps past its timeout is killed and shows a timeout row while the launcher stays responsive to typing. A script exiting non-zero shows its stderr in an error row rather than showing nothing. Do not build yet: No signing or distribution yet. ### Phase 6 · Signing, notarization and updates Build: Enable the hardened runtime, sign with a Developer ID certificate, notarize with notarytool, staple the ticket, and ship updates through Sparkle. Add launch at login with SMAppService. Trap: Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. Done when: A signed and notarized build downloaded on a second Mac opens with no Gatekeeper warning, spctl reports it accepted, and an update served from your appcast installs and relaunches. Do not build yet: Nothing after this. This is the last phase. ### Out of scope (and why) - Cross-device sync. Alfred does not sync either, and building it means a server, accounts and conflict resolution, which is a different product. - The workflow gallery. What you are paying for in the Powerpack is fifteen years of other people's workflows, and no amount of code produces that. - Deep macOS integrations like Contacts, Music control and 1Password lookup. Each is a small piece of work and there are forty of them. - The fifteen years of OS version edge cases behind Alfred's reliability. Every macOS release moves something, and you now own that maintenance. ### README must contain - Which permissions are needed, which feature each unlocks, and the fact that Accessibility and Input Monitoring are separate grants - That clipboard history skips concealed pasteboard types, and what that means for password managers - The workflow JSON line contract with a copyable example - Where the SQLite database lives and how to back it up - That re-signing the app invalidates existing permission grants and how to fix that ===== ARCHITECTURE.md ===== # Architecture · Alfred Powerpack ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI, built in Xcode | the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them | | App type | A background agent with LSUIElement set to true | a launcher with a Dock icon and a menu bar is wrong twice over | | Hotkey | Carbon RegisterEventHotKey | the one API that gets a system-wide hotkey without the Accessibility grant | | Storage | SQLite through the C API in a container directory | history and usage counts are rows; a plist rewritten on every keystroke is not | | Snippet delivery | NSPasteboard plus a synthetic Cmd-V | per-character CGEvent posting drops characters and mangles anything non-ASCII | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | System-wide key registration and the panel show and hide cycle. | Replace Carbon RegisterEventHotKey with a modern event tap only if you already need Accessibility for other reasons; the tap costs you the permission-free property. | | Index | Discovering applications and keeping items current. | Swap the directory scan for an NSMetadataQuery Spotlight query if you want files as well as apps, and accept the indexing latency that comes with it. | | Ranking | Fuzzy match scoring and the frecency multiplier. | The scorer is a pure function of query and item, so an alternative algorithm is a drop-in with a table of expected rankings as its test. | | Clipboard | Polling, concealed-type filtering, and the clip store. | Raise the poll interval or move to a lower-priority queue if battery use shows up in Instruments. | | Workflows | Running user scripts and turning stdout into rows. | The JSON line contract is the boundary. Any language that can print JSON to stdout is already supported. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LAUNCHER_DB_PATH` · required · Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. - `LAUNCHER_CLIP_LIMIT` · required · How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. - `LAUNCHER_WORKFLOW_TIMEOUT_MS` · required · How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. - `SPARKLE_FEED_URL` · optional · Product phase only. The HTTPS URL where you publish your Sparkle appcast. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Alfred Powerpack product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI, built in Xcode, A background agent with LSUIElement set to true, Carbon RegisterEventHotKey, SQLite through the C API in a container directory, NSPasteboard plus a synthetic Cmd-V. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. - Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. ===== MILESTONES.md ===== # Delivery milestones · Alfred Powerpack Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Agent, hotkey and window A background app with no Dock icon that appears on a global hotkey from inside any other application and disappears on Escape. ### Steps 1. Create the Xcode app target and hide it from the Dock File > New > Project > macOS > App, SwiftUI lifecycle. Add the key Application is agent (UIElement) with value YES to Info.plist. Run it: nothing should appear in the Dock. Files: `Info.plist` 2. Add a menu bar item with Quit and Preferences Use MenuBarExtra in SwiftUI. This is the only visible surface until the hotkey works, and it is how you quit a Dock-less app you cannot click. Files: `App.swift` 3. Build the panel as a borderless NSPanel Subclass NSPanel with .nonactivatingPanel and .hudWindow, set canBecomeKey to true, level to .floating and hidesOnDeactivate to true. A plain NSWindow cannot take keyboard input without activating the app. Files: `LauncherPanel.swift` 4. Register the global hotkey Call RegisterEventHotKey with a signature and an EventHotKeyID, and install a Carbon event handler for kEventHotKeyPressed. Store the chosen key combination in UserDefaults so it survives a relaunch. Files: `Hotkey.swift` ### Done when - [ ] The hotkey opens the panel while a different app is frontmost and full screen - [ ] Escape closes the panel and keyboard focus returns to the app that was frontmost - [ ] The app shows no Dock icon and does not appear in the Cmd-Tab switcher - [ ] Quitting from the menu bar item and relaunching restores the configured hotkey ### Watch out - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. ## M2 · App search with frecency Typing two or three letters puts the app you actually meant at the top, and it gets better with use. ### Steps 1. Enumerate application bundles Read the three application directories with FileManager, take every .app bundle, and pull CFBundleDisplayName or CFBundleName plus the bundle identifier from its Info.plist. Store one row per app in items. Files: `Indexer.swift` 2. Write the fuzzy matcher Score a subsequence match: contiguous runs score higher, a match at a word boundary scores higher, and build an acronym from capitalised words so ahk finds Adobe Handbrake Kit. Return a score, not a boolean. Files: `Match.swift` 3. Add the frecency multiplier On every launch insert into usage with the query prefix that produced it. Score an item as matchScore multiplied by (1 plus a decayed sum over its usage rows), halving each row's weight every fourteen days. Files: `Ranking.swift` 4. Rescan on a timer and on application directory changes Rescan every few minutes, and watch the application directories with an FSEvents stream so an install shows up without waiting. Files: `Indexer.swift` ### Done when - [ ] Typing saf puts Safari first - [ ] Launching the second result for the same prefix twice moves it to first place - [ ] An app installed while the launcher is running appears without a relaunch - [ ] A query matching nothing shows an empty state, not the previous results ### Watch out - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. ## M3 · Clipboard history Everything you copy is searchable later, except the things that must never be stored. ### Steps 1. Poll changeCount on a timer A repeating timer at roughly 200ms compares NSPasteboard.general.changeCount to the last value you saw. Only read the pasteboard when the count moved. Files: `ClipboardWatcher.swift` 2. Refuse concealed and transient items Before reading contents, inspect the item types. Skip anything declaring org.nspasteboard.ConcealedType, org.nspasteboard.TransientType or org.nspasteboard.AutoGeneratedType. Write a test that feeds a synthetic concealed item and asserts nothing was stored. Files: `ClipboardWatcher.swift` 3. Store with a content hash Hash the text or image bytes. On a hash you already hold, update copied_at instead of inserting. Cap history by row count and by total bytes, and delete oldest first, never pinned rows. Files: `ClipStore.swift` 4. Add history mode and paste back A second registered hotkey opens the panel filtered to clips. Selecting a row writes it to the pasteboard and posts Cmd-V. This is where the Accessibility grant is first needed, so explain it in the prompt you show. Files: `Paste.swift` ### Done when - [ ] Three copies produce three rows, newest first - [ ] Copying the same text twice leaves one row with a moved timestamp - [ ] A password copied out of a password manager is absent from the history - [ ] Selecting a history row pastes it into the previously focused app ### Watch out - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. ## M4 · Snippets A trigger you type anywhere expands into a block of text, without dropping characters. ### Steps 1. Add a global key monitor behind the Accessibility grant NSEvent.addGlobalMonitorForEvents for keyDown requires Accessibility. Check AXIsProcessTrustedWithOptions and show your own explanation before the system prompt appears. Files: `SnippetWatcher.swift` 2. Match triggers against a rolling buffer Keep a short rolling buffer of recent characters and reset it on Return, Escape, a click or a focus change. Match the longest trigger that ends at the buffer tail. Files: `SnippetWatcher.swift` 3. Expand through the pasteboard and restore Save the current pasteboard items, send backspaces for the trigger length, write the body, send Cmd-V, then restore the saved items after a short delay. Restoring too early pastes the wrong thing. Files: `Expand.swift` 4. Detect and report a refused expansion If the frontmost element is a secure text field, do not attempt the expansion. Show a brief notification saying why. Files: `Expand.swift` ### Done when - [ ] A trigger expands in TextEdit with an emoji and an accented character intact - [ ] The pasteboard after an expansion holds what it held before - [ ] A trigger typed into a password field leaves the field untouched and shows a refusal - [ ] Expansion count increments on the snippet row ### Watch out - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. ## M5 · Script workflows Your own scripts become result providers, so the launcher does things this build never anticipated. ### Steps 1. Define the JSON line contract and document it One JSON object per line with title, subtitle, an optional icon path and an arg passed back on selection. Write the contract in the README before implementing it. Files: `docs/workflows.md` 2. Run scripts with a timeout and cancellation Use Process with a dispatch queue. Cancel and kill the previous run when a new keystroke arrives, so an abandoned query never outlives its input. Files: `WorkflowRunner.swift` 3. Render results and handle selection Map JSON lines to rows. On selection re-invoke the script with the arg and a mode flag so it can act rather than list. Files: `WorkflowRunner.swift` 4. Ship two example workflows A directory search using mdfind and a unit converter. They prove the contract and give the user something to copy. Files: `examples/` ### Done when - [ ] A script printing two JSON lines renders two rows - [ ] A script that sleeps past the timeout is killed and the launcher keeps accepting input - [ ] A non-zero exit shows an error row carrying the stderr text - [ ] Typing quickly cancels the previous run rather than queueing runs ### Watch out - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ## M6 · Signing, notarization and updates (production only) The app installs on a machine that is not yours without a Gatekeeper warning, and can ship a fix. ### Steps 1. Enable the hardened runtime and sign Turn on the hardened runtime in Signing and Capabilities. Sign with Developer ID Application, which requires a paid Apple Developer account. ```sh codesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Launcher.app ``` 2. Notarize and staple Submit the zipped app with notarytool, wait for acceptance, then staple the ticket so it validates offline. ```sh xcrun notarytool submit Launcher.zip --keychain-profile AC --wait xcrun stapler staple Launcher.app ``` 3. Add Sparkle with a signed appcast Generate an EdDSA key pair, keep the private key out of the repository, and publish appcast.xml over HTTPS. Files: `appcast.xml` 4. Add launch at login SMAppService.mainApp.register() from a settings toggle. The old login item APIs are deprecated and behave differently under sandboxing. Files: `Settings.swift` ### Done when - [ ] spctl --assess --verbose reports the app accepted - [ ] A download on a second Mac opens with no Gatekeeper warning - [ ] An update published to the appcast installs and relaunches - [ ] Launch at login survives a reboot ### Watch out - Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. ===== OPERATIONS.md ===== # Operations · Alfred Powerpack ## Backup The SQLite database under Application Support is the entire state. Copy it while the app is not running, or use the SQLite backup API. Snippets and pinned clips are the parts people would actually miss. ## Restore Quit the app, replace the database file, relaunch, and confirm the snippet count and pinned clip count match what you backed up. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring There is no server to monitor. What matters is battery and CPU: profile the clipboard poll and the keystroke monitor in Instruments before release, and again after any change to either. ## Incident checklist The realistic incident is a macOS update breaking a permission or an API. Keep the previous signed build downloadable, and put the minimum supported macOS version in the appcast so an incompatible update is never offered. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] spctl accepts the notarized build and it opens clean on a Mac that never had it installed - [ ] A concealed pasteboard item is still refused by the test written in Phase 3 - [ ] Snippet expansion is verified in at least three different text engines, including a browser - [ ] Instruments shows idle CPU under one percent with the clipboard watcher running - [ ] The README states every permission and what refusing it costs ## Launch constraint Do not market omitted Alfred Powerpack capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. LAUNCHER_DB_PATH=~/Library/Application Support/Launcher/launcher.db # Required. How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. LAUNCHER_CLIP_LIMIT=500 # Required. How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. LAUNCHER_WORKFLOW_TIMEOUT_MS=1500 # Optional. Product phase only. The HTTPS URL where you publish your Sparkle appcast. SPARKLE_FEED_URL=https://example.com/appcast.xml
# Alfred Powerpack · indie build A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI, built in Xcode | the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them | | App type | A background agent with LSUIElement set to true | a launcher with a Dock icon and a menu bar is wrong twice over | | Hotkey | Carbon RegisterEventHotKey | the one API that gets a system-wide hotkey without the Accessibility grant | | Storage | SQLite through the C API in a container directory | history and usage counts are rows; a plist rewritten on every keystroke is not | | Snippet delivery | NSPasteboard plus a synthetic Cmd-V | per-character CGEvent posting drops characters and mangles anything non-ASCII | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you already own a Mac - Why: The whole build is AppKit. There is no way to develop or test this on another platform. - Get it: Any Mac from the last few years running Sonoma or later. Check with the Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds the app, signs it, and provides the Instruments profiler you will want when the keystroke monitor feels slow. - Get it: Install from the Mac App Store, or download from developer.apple.com/xcode. It is a large download, so start it before you read further. - Verify: xcodebuild -version prints 16 or higher - [ ] **Comfort with Swift and a little AppKit** · free - Why: SwiftUI covers the panel, but hotkeys, the pasteboard and event monitors are all AppKit and Carbon. You will be reading Apple documentation, not tutorials. - Get it: If you have never written Swift, build the panel in Phase 1 by itself and stop there for a day before continuing. - [ ] **Willingness to grant Accessibility to your own debug build** · free - Why: Snippets and paste-back cannot work without it. You are granting a program you compiled the ability to read keystrokes, so understand what that means before you turn it on. - Get it: System Settings > Privacy and Security > Accessibility, then add your built app. During development this must be re-granted whenever the binary path changes. - Verify: AXIsProcessTrusted() returns true from the running app - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Only needed for the product phase. Without it you cannot get a Developer ID certificate, so you cannot notarize, so the app shows a Gatekeeper warning on anyone else's Mac. - Get it: Enrol at developer.apple.com/programs. Enrolment is reviewed and can take a day or two. - Verify: A Developer ID Application certificate appears in Xcode > Settings > Accounts ## Quick start ```sh codesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Launcher.app ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. - years of macOS integration, workflow ecosystem, reliability, and community - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Alfred Powerpack, and the README should say so rather than pretend.
# Build brief · Alfred Powerpack The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first launcher for macOS with app search, clipboard history, snippets and script workflows to replace Alfred Powerpack. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI, built in Xcode · the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them - App type: A background agent with LSUIElement set to true · a launcher with a Dock icon and a menu bar is wrong twice over - Hotkey: Carbon RegisterEventHotKey · the one API that gets a system-wide hotkey without the Accessibility grant - Storage: SQLite through the C API in a container directory · history and usage counts are rows; a plist rewritten on every keystroke is not - Snippet delivery: NSPasteboard plus a synthetic Cmd-V · per-character CGEvent posting drops characters and mangles anything non-ASCII ### Data model (create this before Phase 1) - items(id, kind, title, subtitle, path, bundle_id, indexed_at) · kind is app, file or workflow_result - usage(item_id, used_at, query_prefix) · one row per launch, the input to frecency - clips(id, kind, content, preview, source_bundle_id, hash, copied_at, pinned) - snippets(id, trigger, body, expand_count, last_used_at) - workflows(id, name, keyword, script_path, timeout_ms, enabled) - Index items on (kind, title) and clips on (copied_at desc); the query runs on every keystroke. ### Phase 1 · Agent, hotkey and window Build: An Xcode app target with LSUIElement true in Info.plist, a menu bar item, and a borderless floating NSPanel that becomes key without stealing focus permanently. Register a single global hotkey with RegisterEventHotKey and make it configurable. The panel shows a text field and an empty result list. Nothing searches yet. Trap: RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. Done when: The hotkey opens the panel while a full-screen app is frontmost, Escape closes it and returns focus to that same app, and the process has no Dock icon and no window in the app switcher. Do not build yet: No searching, no clipboard capture, no scripts. A window that appears and disappears reliably is the whole phase. ### Phase 2 · App search with frecency Build: Scan /Applications, /System/Applications and ~/Applications for bundles, read CFBundleDisplayName and the icon, and store them in items. Fuzzy match the query against title and an acronym of its words. Rank by match quality multiplied by a frecency score built from the usage table: recent launches weigh more than old ones, and a launch after the same query prefix weighs most of all. Trap: Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. Done when: Typing saf ranks Safari first. Launching a lower-ranked result twice for the same prefix promotes it above the previous winner. A newly installed app appears without a relaunch, within one rescan interval. Do not build yet: No file search, no clipboard, no scripts. Applications only. ### Phase 3 · Clipboard history Build: Poll NSPasteboard.general.changeCount on a timer and capture new contents into clips. Store text and image data with a hash so a repeated copy updates a timestamp rather than adding a duplicate row. Add a second hotkey that opens the panel in history mode, and pasting a row puts it back on the pasteboard and sends Cmd-V. Trap: NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. Trap: Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. Done when: Copying three things and opening history shows three rows, newest first. Copying the same string twice leaves one row with an updated timestamp. A password copied from a password manager does not appear in the history at all. Do not build yet: No snippets and no scripts. Capture, search and paste back. ### Phase 4 · Snippets Build: Watch for trigger strings with a global key event monitor, delete the trigger with the right number of backspaces, then put the body on the pasteboard and send Cmd-V. Restore the user's previous pasteboard contents afterwards. Trap: Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. Trap: Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. Done when: Typing a configured trigger in a plain text editor replaces it with the body, including an emoji and an accented character, with no characters lost. The same trigger in a password field leaves the field untouched and surfaces a visible refusal. The pasteboard afterwards holds what it held before the expansion. Do not build yet: No script workflows yet. ### Phase 5 · Script workflows Build: A workflow is a keyword plus an executable. When the query starts with the keyword, run the script with the rest of the query as its single argument, read JSON lines from stdout, and render each as a selectable row. Selecting a row runs the script again with the row's action payload. Trap: Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. Done when: A script printing two JSON lines shows two selectable rows. A script that sleeps past its timeout is killed and shows a timeout row while the launcher stays responsive to typing. A script exiting non-zero shows its stderr in an error row rather than showing nothing. Do not build yet: No signing or distribution yet. ### Phase 6 · Signing, notarization and updates Build: Enable the hardened runtime, sign with a Developer ID certificate, notarize with notarytool, staple the ticket, and ship updates through Sparkle. Add launch at login with SMAppService. Trap: Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. Done when: A signed and notarized build downloaded on a second Mac opens with no Gatekeeper warning, spctl reports it accepted, and an update served from your appcast installs and relaunches. Do not build yet: Nothing after this. This is the last phase. ### Out of scope (and why) - Cross-device sync. Alfred does not sync either, and building it means a server, accounts and conflict resolution, which is a different product. - The workflow gallery. What you are paying for in the Powerpack is fifteen years of other people's workflows, and no amount of code produces that. - Deep macOS integrations like Contacts, Music control and 1Password lookup. Each is a small piece of work and there are forty of them. - The fifteen years of OS version edge cases behind Alfred's reliability. Every macOS release moves something, and you now own that maintenance. ### README must contain - Which permissions are needed, which feature each unlocks, and the fact that Accessibility and Input Monitoring are separate grants - That clipboard history skips concealed pasteboard types, and what that means for password managers - The workflow JSON line contract with a copyable example - Where the SQLite database lives and how to back it up - That re-signing the app invalidates existing permission grants and how to fix that
# Agent instructions · Alfred Powerpack indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI, built in Xcode, A background agent with LSUIElement set to true, Carbon RegisterEventHotKey, SQLite through the C API in a container directory, NSPasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke.
# Build plan · Alfred Powerpack A menu bar agent that opens on a global hotkey, finds and launches apps by fuzzy match with frecency ranking, keeps a searchable clipboard history that respects concealed pasteboard types, expands text snippets through the pasteboard, and runs your own shell scripts as result providers. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Agent, hotkey and window A background app with no Dock icon that appears on a global hotkey from inside any other application and disappears on Escape. ### Steps 1. Create the Xcode app target and hide it from the Dock File > New > Project > macOS > App, SwiftUI lifecycle. Add the key Application is agent (UIElement) with value YES to Info.plist. Run it: nothing should appear in the Dock. Files: `Info.plist` 2. Add a menu bar item with Quit and Preferences Use MenuBarExtra in SwiftUI. This is the only visible surface until the hotkey works, and it is how you quit a Dock-less app you cannot click. Files: `App.swift` 3. Build the panel as a borderless NSPanel Subclass NSPanel with .nonactivatingPanel and .hudWindow, set canBecomeKey to true, level to .floating and hidesOnDeactivate to true. A plain NSWindow cannot take keyboard input without activating the app. Files: `LauncherPanel.swift` 4. Register the global hotkey Call RegisterEventHotKey with a signature and an EventHotKeyID, and install a Carbon event handler for kEventHotKeyPressed. Store the chosen key combination in UserDefaults so it survives a relaunch. Files: `Hotkey.swift` ### Done when - [ ] The hotkey opens the panel while a different app is frontmost and full screen - [ ] Escape closes the panel and keyboard focus returns to the app that was frontmost - [ ] The app shows no Dock icon and does not appear in the Cmd-Tab switcher - [ ] Quitting from the menu bar item and relaunching restores the configured hotkey ### Watch out - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. ## Phase 2 · App search with frecency Typing two or three letters puts the app you actually meant at the top, and it gets better with use. ### Steps 1. Enumerate application bundles Read the three application directories with FileManager, take every .app bundle, and pull CFBundleDisplayName or CFBundleName plus the bundle identifier from its Info.plist. Store one row per app in items. Files: `Indexer.swift` 2. Write the fuzzy matcher Score a subsequence match: contiguous runs score higher, a match at a word boundary scores higher, and build an acronym from capitalised words so ahk finds Adobe Handbrake Kit. Return a score, not a boolean. Files: `Match.swift` 3. Add the frecency multiplier On every launch insert into usage with the query prefix that produced it. Score an item as matchScore multiplied by (1 plus a decayed sum over its usage rows), halving each row's weight every fourteen days. Files: `Ranking.swift` 4. Rescan on a timer and on application directory changes Rescan every few minutes, and watch the application directories with an FSEvents stream so an install shows up without waiting. Files: `Indexer.swift` ### Done when - [ ] Typing saf puts Safari first - [ ] Launching the second result for the same prefix twice moves it to first place - [ ] An app installed while the launcher is running appears without a relaunch - [ ] A query matching nothing shows an empty state, not the previous results ### Watch out - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. ## Phase 3 · Clipboard history Everything you copy is searchable later, except the things that must never be stored. ### Steps 1. Poll changeCount on a timer A repeating timer at roughly 200ms compares NSPasteboard.general.changeCount to the last value you saw. Only read the pasteboard when the count moved. Files: `ClipboardWatcher.swift` 2. Refuse concealed and transient items Before reading contents, inspect the item types. Skip anything declaring org.nspasteboard.ConcealedType, org.nspasteboard.TransientType or org.nspasteboard.AutoGeneratedType. Write a test that feeds a synthetic concealed item and asserts nothing was stored. Files: `ClipboardWatcher.swift` 3. Store with a content hash Hash the text or image bytes. On a hash you already hold, update copied_at instead of inserting. Cap history by row count and by total bytes, and delete oldest first, never pinned rows. Files: `ClipStore.swift` 4. Add history mode and paste back A second registered hotkey opens the panel filtered to clips. Selecting a row writes it to the pasteboard and posts Cmd-V. This is where the Accessibility grant is first needed, so explain it in the prompt you show. Files: `Paste.swift` ### Done when - [ ] Three copies produce three rows, newest first - [ ] Copying the same text twice leaves one row with a moved timestamp - [ ] A password copied out of a password manager is absent from the history - [ ] Selecting a history row pastes it into the previously focused app ### Watch out - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. ## Phase 4 · Snippets A trigger you type anywhere expands into a block of text, without dropping characters. ### Steps 1. Add a global key monitor behind the Accessibility grant NSEvent.addGlobalMonitorForEvents for keyDown requires Accessibility. Check AXIsProcessTrustedWithOptions and show your own explanation before the system prompt appears. Files: `SnippetWatcher.swift` 2. Match triggers against a rolling buffer Keep a short rolling buffer of recent characters and reset it on Return, Escape, a click or a focus change. Match the longest trigger that ends at the buffer tail. Files: `SnippetWatcher.swift` 3. Expand through the pasteboard and restore Save the current pasteboard items, send backspaces for the trigger length, write the body, send Cmd-V, then restore the saved items after a short delay. Restoring too early pastes the wrong thing. Files: `Expand.swift` 4. Detect and report a refused expansion If the frontmost element is a secure text field, do not attempt the expansion. Show a brief notification saying why. Files: `Expand.swift` ### Done when - [ ] A trigger expands in TextEdit with an emoji and an accented character intact - [ ] The pasteboard after an expansion holds what it held before - [ ] A trigger typed into a password field leaves the field untouched and shows a refusal - [ ] Expansion count increments on the snippet row ### Watch out - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. ## Phase 5 · Script workflows Your own scripts become result providers, so the launcher does things this build never anticipated. ### Steps 1. Define the JSON line contract and document it One JSON object per line with title, subtitle, an optional icon path and an arg passed back on selection. Write the contract in the README before implementing it. Files: `docs/workflows.md` 2. Run scripts with a timeout and cancellation Use Process with a dispatch queue. Cancel and kill the previous run when a new keystroke arrives, so an abandoned query never outlives its input. Files: `WorkflowRunner.swift` 3. Render results and handle selection Map JSON lines to rows. On selection re-invoke the script with the arg and a mode flag so it can act rather than list. Files: `WorkflowRunner.swift` 4. Ship two example workflows A directory search using mdfind and a unit converter. They prove the contract and give the user something to copy. Files: `examples/` ### Done when - [ ] A script printing two JSON lines renders two rows - [ ] A script that sleeps past the timeout is killed and the launcher keeps accepting input - [ ] A non-zero exit shows an error row carrying the stderr text - [ ] Typing quickly cancels the previous run rather than queueing runs ### Watch out - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ## Not in this build - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. ## After v1, if you want it - File search with NSMetadataQuery, once you accept the permission prompts it brings - A calculator result row using NSExpression - Sharing workflows as a signed folder others can drop in
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. LAUNCHER_DB_PATH=~/Library/Application Support/Launcher/launcher.db # Required. How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. LAUNCHER_CLIP_LIMIT=500 # Required. How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. LAUNCHER_WORKFLOW_TIMEOUT_MS=1500 # Optional. Product phase only. The HTTPS URL where you publish your Sparkle appcast. SPARKLE_FEED_URL=https://example.com/appcast.xml
# Alfred Powerpack · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Alfred Powerpack, build a local launcher with search, snippets, clipboard history, and workflows. The hard boundary is years of macos integration, workflow ecosystem, reliability, and community, plus os integration polish and maintenance. ## Product outcome A signed, notarized launcher that other people can install, that updates itself, and whose permission requirements are documented well enough that a stranger grants them knowingly. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - Cross-device sync of clips or snippets. There is no server here by design. - The Alfred workflow gallery, which is the actual reason many people pay. - Deep integrations with Contacts, Music, Mail and password managers. - Scrolling file previews and the file navigation system Alfred grew over a decade. - years of macOS integration, workflow ecosystem, reliability, and community - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - spctl accepts the notarized build and it opens clean on a Mac that never had it installed - A concealed pasteboard item is still refused by the test written in Phase 3 - Snippet expansion is verified in at least three different text engines, including a browser - Instruments shows idle CPU under one percent with the clipboard watcher running - The README states every permission and what refusing it costs
# Build brief · Alfred Powerpack The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first launcher for macOS with app search, clipboard history, snippets and script workflows to replace Alfred Powerpack. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI, built in Xcode · the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them - App type: A background agent with LSUIElement set to true · a launcher with a Dock icon and a menu bar is wrong twice over - Hotkey: Carbon RegisterEventHotKey · the one API that gets a system-wide hotkey without the Accessibility grant - Storage: SQLite through the C API in a container directory · history and usage counts are rows; a plist rewritten on every keystroke is not - Snippet delivery: NSPasteboard plus a synthetic Cmd-V · per-character CGEvent posting drops characters and mangles anything non-ASCII ### Data model (create this before Phase 1) - items(id, kind, title, subtitle, path, bundle_id, indexed_at) · kind is app, file or workflow_result - usage(item_id, used_at, query_prefix) · one row per launch, the input to frecency - clips(id, kind, content, preview, source_bundle_id, hash, copied_at, pinned) - snippets(id, trigger, body, expand_count, last_used_at) - workflows(id, name, keyword, script_path, timeout_ms, enabled) - Index items on (kind, title) and clips on (copied_at desc); the query runs on every keystroke. ### Phase 1 · Agent, hotkey and window Build: An Xcode app target with LSUIElement true in Info.plist, a menu bar item, and a borderless floating NSPanel that becomes key without stealing focus permanently. Register a single global hotkey with RegisterEventHotKey and make it configurable. The panel shows a text field and an empty result list. Nothing searches yet. Trap: RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. Done when: The hotkey opens the panel while a full-screen app is frontmost, Escape closes it and returns focus to that same app, and the process has no Dock icon and no window in the app switcher. Do not build yet: No searching, no clipboard capture, no scripts. A window that appears and disappears reliably is the whole phase. ### Phase 2 · App search with frecency Build: Scan /Applications, /System/Applications and ~/Applications for bundles, read CFBundleDisplayName and the icon, and store them in items. Fuzzy match the query against title and an acronym of its words. Rank by match quality multiplied by a frecency score built from the usage table: recent launches weigh more than old ones, and a launch after the same query prefix weighs most of all. Trap: Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. Done when: Typing saf ranks Safari first. Launching a lower-ranked result twice for the same prefix promotes it above the previous winner. A newly installed app appears without a relaunch, within one rescan interval. Do not build yet: No file search, no clipboard, no scripts. Applications only. ### Phase 3 · Clipboard history Build: Poll NSPasteboard.general.changeCount on a timer and capture new contents into clips. Store text and image data with a hash so a repeated copy updates a timestamp rather than adding a duplicate row. Add a second hotkey that opens the panel in history mode, and pasting a row puts it back on the pasteboard and sends Cmd-V. Trap: NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. Trap: Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. Done when: Copying three things and opening history shows three rows, newest first. Copying the same string twice leaves one row with an updated timestamp. A password copied from a password manager does not appear in the history at all. Do not build yet: No snippets and no scripts. Capture, search and paste back. ### Phase 4 · Snippets Build: Watch for trigger strings with a global key event monitor, delete the trigger with the right number of backspaces, then put the body on the pasteboard and send Cmd-V. Restore the user's previous pasteboard contents afterwards. Trap: Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. Trap: Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. Done when: Typing a configured trigger in a plain text editor replaces it with the body, including an emoji and an accented character, with no characters lost. The same trigger in a password field leaves the field untouched and surfaces a visible refusal. The pasteboard afterwards holds what it held before the expansion. Do not build yet: No script workflows yet. ### Phase 5 · Script workflows Build: A workflow is a keyword plus an executable. When the query starts with the keyword, run the script with the rest of the query as its single argument, read JSON lines from stdout, and render each as a selectable row. Selecting a row runs the script again with the row's action payload. Trap: Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. Done when: A script printing two JSON lines shows two selectable rows. A script that sleeps past its timeout is killed and shows a timeout row while the launcher stays responsive to typing. A script exiting non-zero shows its stderr in an error row rather than showing nothing. Do not build yet: No signing or distribution yet. ### Phase 6 · Signing, notarization and updates Build: Enable the hardened runtime, sign with a Developer ID certificate, notarize with notarytool, staple the ticket, and ship updates through Sparkle. Add launch at login with SMAppService. Trap: Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity. Done when: A signed and notarized build downloaded on a second Mac opens with no Gatekeeper warning, spctl reports it accepted, and an update served from your appcast installs and relaunches. Do not build yet: Nothing after this. This is the last phase. ### Out of scope (and why) - Cross-device sync. Alfred does not sync either, and building it means a server, accounts and conflict resolution, which is a different product. - The workflow gallery. What you are paying for in the Powerpack is fifteen years of other people's workflows, and no amount of code produces that. - Deep macOS integrations like Contacts, Music control and 1Password lookup. Each is a small piece of work and there are forty of them. - The fifteen years of OS version edge cases behind Alfred's reliability. Every macOS release moves something, and you now own that maintenance. ### README must contain - Which permissions are needed, which feature each unlocks, and the fact that Accessibility and Input Monitoring are separate grants - That clipboard history skips concealed pasteboard types, and what that means for password managers - The workflow JSON line contract with a copyable example - Where the SQLite database lives and how to back it up - That re-signing the app invalidates existing permission grants and how to fix that
# Architecture · Alfred Powerpack ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI, built in Xcode | the permission, hotkey and pasteboard APIs are all AppKit; a cross-platform toolkit cannot reach them | | App type | A background agent with LSUIElement set to true | a launcher with a Dock icon and a menu bar is wrong twice over | | Hotkey | Carbon RegisterEventHotKey | the one API that gets a system-wide hotkey without the Accessibility grant | | Storage | SQLite through the C API in a container directory | history and usage counts are rows; a plist rewritten on every keystroke is not | | Snippet delivery | NSPasteboard plus a synthetic Cmd-V | per-character CGEvent posting drops characters and mangles anything non-ASCII | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | System-wide key registration and the panel show and hide cycle. | Replace Carbon RegisterEventHotKey with a modern event tap only if you already need Accessibility for other reasons; the tap costs you the permission-free property. | | Index | Discovering applications and keeping items current. | Swap the directory scan for an NSMetadataQuery Spotlight query if you want files as well as apps, and accept the indexing latency that comes with it. | | Ranking | Fuzzy match scoring and the frecency multiplier. | The scorer is a pure function of query and item, so an alternative algorithm is a drop-in with a table of expected rankings as its test. | | Clipboard | Polling, concealed-type filtering, and the clip store. | Raise the poll interval or move to a lower-priority queue if battery use shows up in Instruments. | | Workflows | Running user scripts and turning stdout into rows. | The JSON line contract is the boundary. Any language that can print JSON to stdout is already supported. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LAUNCHER_DB_PATH` · required · Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. - `LAUNCHER_CLIP_LIMIT` · required · How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. - `LAUNCHER_WORKFLOW_TIMEOUT_MS` · required · How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. - `SPARKLE_FEED_URL` · optional · Product phase only. The HTTPS URL where you publish your Sparkle appcast. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Alfred Powerpack product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI, built in Xcode, A background agent with LSUIElement set to true, Carbon RegisterEventHotKey, SQLite through the C API in a container directory, NSPasteboard plus a synthetic Cmd-V. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. - Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity.
# Delivery milestones · Alfred Powerpack Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Agent, hotkey and window A background app with no Dock icon that appears on a global hotkey from inside any other application and disappears on Escape. ### Steps 1. Create the Xcode app target and hide it from the Dock File > New > Project > macOS > App, SwiftUI lifecycle. Add the key Application is agent (UIElement) with value YES to Info.plist. Run it: nothing should appear in the Dock. Files: `Info.plist` 2. Add a menu bar item with Quit and Preferences Use MenuBarExtra in SwiftUI. This is the only visible surface until the hotkey works, and it is how you quit a Dock-less app you cannot click. Files: `App.swift` 3. Build the panel as a borderless NSPanel Subclass NSPanel with .nonactivatingPanel and .hudWindow, set canBecomeKey to true, level to .floating and hidesOnDeactivate to true. A plain NSWindow cannot take keyboard input without activating the app. Files: `LauncherPanel.swift` 4. Register the global hotkey Call RegisterEventHotKey with a signature and an EventHotKeyID, and install a Carbon event handler for kEventHotKeyPressed. Store the chosen key combination in UserDefaults so it survives a relaunch. Files: `Hotkey.swift` ### Done when - [ ] The hotkey opens the panel while a different app is frontmost and full screen - [ ] Escape closes the panel and keyboard focus returns to the app that was frontmost - [ ] The app shows no Dock icon and does not appear in the Cmd-Tab switcher - [ ] Quitting from the menu bar item and relaunching restores the configured hotkey ### Watch out - RegisterEventHotKey needs no permission at all. Sending keystrokes into other apps later does, and Accessibility is a separate grant from Input Monitoring. Do not ask for either in this phase or you will train the user to refuse the one that matters. ## M2 · App search with frecency Typing two or three letters puts the app you actually meant at the top, and it gets better with use. ### Steps 1. Enumerate application bundles Read the three application directories with FileManager, take every .app bundle, and pull CFBundleDisplayName or CFBundleName plus the bundle identifier from its Info.plist. Store one row per app in items. Files: `Indexer.swift` 2. Write the fuzzy matcher Score a subsequence match: contiguous runs score higher, a match at a word boundary scores higher, and build an acronym from capitalised words so ahk finds Adobe Handbrake Kit. Return a score, not a boolean. Files: `Match.swift` 3. Add the frecency multiplier On every launch insert into usage with the query prefix that produced it. Score an item as matchScore multiplied by (1 plus a decayed sum over its usage rows), halving each row's weight every fourteen days. Files: `Ranking.swift` 4. Rescan on a timer and on application directory changes Rescan every few minutes, and watch the application directories with an FSEvents stream so an install shows up without waiting. Files: `Indexer.swift` ### Done when - [ ] Typing saf puts Safari first - [ ] Launching the second result for the same prefix twice moves it to first place - [ ] An app installed while the launcher is running appears without a relaunch - [ ] A query matching nothing shows an empty state, not the previous results ### Watch out - Do not walk the whole disk. A recursive scan of a home directory takes minutes, hits permission dialogs for Documents and Desktop, and buys you nothing a bundle scan of three directories does not. ## M3 · Clipboard history Everything you copy is searchable later, except the things that must never be stored. ### Steps 1. Poll changeCount on a timer A repeating timer at roughly 200ms compares NSPasteboard.general.changeCount to the last value you saw. Only read the pasteboard when the count moved. Files: `ClipboardWatcher.swift` 2. Refuse concealed and transient items Before reading contents, inspect the item types. Skip anything declaring org.nspasteboard.ConcealedType, org.nspasteboard.TransientType or org.nspasteboard.AutoGeneratedType. Write a test that feeds a synthetic concealed item and asserts nothing was stored. Files: `ClipboardWatcher.swift` 3. Store with a content hash Hash the text or image bytes. On a hash you already hold, update copied_at instead of inserting. Cap history by row count and by total bytes, and delete oldest first, never pinned rows. Files: `ClipStore.swift` 4. Add history mode and paste back A second registered hotkey opens the panel filtered to clips. Selecting a row writes it to the pasteboard and posts Cmd-V. This is where the Accessibility grant is first needed, so explain it in the prompt you show. Files: `Paste.swift` ### Done when - [ ] Three copies produce three rows, newest first - [ ] Copying the same text twice leaves one row with a moved timestamp - [ ] A password copied out of a password manager is absent from the history - [ ] Selecting a history row pastes it into the previously focused app ### Watch out - NSPasteboard has no change notification of any kind. Polling changeCount is not a workaround, it is the supported approach; every clipboard manager on the platform does it. - Password managers mark their pasteboard items with org.nspasteboard.ConcealedType and similar transient types. If you ignore those markers your history quietly becomes a plaintext password log. Skip any item carrying a concealed, transient or autogenerated type. ## M4 · Snippets A trigger you type anywhere expands into a block of text, without dropping characters. ### Steps 1. Add a global key monitor behind the Accessibility grant NSEvent.addGlobalMonitorForEvents for keyDown requires Accessibility. Check AXIsProcessTrustedWithOptions and show your own explanation before the system prompt appears. Files: `SnippetWatcher.swift` 2. Match triggers against a rolling buffer Keep a short rolling buffer of recent characters and reset it on Return, Escape, a click or a focus change. Match the longest trigger that ends at the buffer tail. Files: `SnippetWatcher.swift` 3. Expand through the pasteboard and restore Save the current pasteboard items, send backspaces for the trigger length, write the body, send Cmd-V, then restore the saved items after a short delay. Restoring too early pastes the wrong thing. Files: `Expand.swift` 4. Detect and report a refused expansion If the frontmost element is a secure text field, do not attempt the expansion. Show a brief notification saying why. Files: `Expand.swift` ### Done when - [ ] A trigger expands in TextEdit with an emoji and an accented character intact - [ ] The pasteboard after an expansion holds what it held before - [ ] A trigger typed into a password field leaves the field untouched and shows a refusal - [ ] Expansion count increments on the snippet row ### Watch out - Do not type the body with one CGEvent per character. It drops characters under load, mangles anything outside ASCII, and behaves differently in every text engine. Paste and restore is the approach that works. - Secure text fields refuse synthetic input by design. Detect the failure and show it, rather than leaving the user staring at a field where nothing happened. ## M5 · Script workflows Your own scripts become result providers, so the launcher does things this build never anticipated. ### Steps 1. Define the JSON line contract and document it One JSON object per line with title, subtitle, an optional icon path and an arg passed back on selection. Write the contract in the README before implementing it. Files: `docs/workflows.md` 2. Run scripts with a timeout and cancellation Use Process with a dispatch queue. Cancel and kill the previous run when a new keystroke arrives, so an abandoned query never outlives its input. Files: `WorkflowRunner.swift` 3. Render results and handle selection Map JSON lines to rows. On selection re-invoke the script with the arg and a mode flag so it can act rather than list. Files: `WorkflowRunner.swift` 4. Ship two example workflows A directory search using mdfind and a unit converter. They prove the contract and give the user something to copy. Files: `examples/` ### Done when - [ ] A script printing two JSON lines renders two rows - [ ] A script that sleeps past the timeout is killed and the launcher keeps accepting input - [ ] A non-zero exit shows an error row carrying the stderr text - [ ] Typing quickly cancels the previous run rather than queueing runs ### Watch out - Run the script off the main thread with a hard timeout and kill the process group on timeout, not just the child. A script that blocks on stdin will otherwise hang the whole launcher on every keystroke. ## M6 · Signing, notarization and updates (production only) The app installs on a machine that is not yours without a Gatekeeper warning, and can ship a fix. ### Steps 1. Enable the hardened runtime and sign Turn on the hardened runtime in Signing and Capabilities. Sign with Developer ID Application, which requires a paid Apple Developer account. ```sh codesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Launcher.app ``` 2. Notarize and staple Submit the zipped app with notarytool, wait for acceptance, then staple the ticket so it validates offline. ```sh xcrun notarytool submit Launcher.zip --keychain-profile AC --wait xcrun stapler staple Launcher.app ``` 3. Add Sparkle with a signed appcast Generate an EdDSA key pair, keep the private key out of the repository, and publish appcast.xml over HTTPS. Files: `appcast.xml` 4. Add launch at login SMAppService.mainApp.register() from a settings toggle. The old login item APIs are deprecated and behave differently under sandboxing. Files: `Settings.swift` ### Done when - [ ] spctl --assess --verbose reports the app accepted - [ ] A download on a second Mac opens with no Gatekeeper warning - [ ] An update published to the appcast installs and relaunches - [ ] Launch at login survives a reboot ### Watch out - Screen and Accessibility permissions are granted to a specific code signature. Re-signing with a different identity silently invalidates the grant and macOS will not prompt again; the user has to remove the app in System Settings first. Say this in your release notes the first time you change identity.
# Operations · Alfred Powerpack ## Backup The SQLite database under Application Support is the entire state. Copy it while the app is not running, or use the SQLite backup API. Snippets and pinned clips are the parts people would actually miss. ## Restore Quit the app, replace the database file, relaunch, and confirm the snippet count and pinned clip count match what you backed up. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring There is no server to monitor. What matters is battery and CPU: profile the clipboard poll and the keystroke monitor in Instruments before release, and again after any change to either. ## Incident checklist The realistic incident is a macOS update breaking a permission or an API. Keep the previous signed build downloadable, and put the minimum supported macOS version in the appcast so an incompatible update is never offered. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] spctl accepts the notarized build and it opens clean on a Mac that never had it installed - [ ] A concealed pasteboard item is still refused by the test written in Phase 3 - [ ] Snippet expansion is verified in at least three different text engines, including a browser - [ ] Instruments shows idle CPU under one percent with the clipboard watcher running - [ ] The README states every permission and what refusing it costs ## Launch constraint Do not market omitted Alfred Powerpack capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where the SQLite database lives. Keep it under Application Support so a Time Machine backup includes it. LAUNCHER_DB_PATH=~/Library/Application Support/Launcher/launcher.db # Required. How many clipboard rows to keep. Chosen by you; the oldest unpinned rows are deleted past this. LAUNCHER_CLIP_LIMIT=500 # Required. How long a workflow script may run before it is killed. Keep it short; this runs on every keystroke. LAUNCHER_WORKFLOW_TIMEOUT_MS=1500 # Optional. Product phase only. The HTTPS URL where you publish your Sparkle appcast. SPARKLE_FEED_URL=https://example.com/appcast.xml
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Alfred Powerpack because small utilities are technically reproducible, but customers pay for polish and immediate fixes when an OS update breaks a daily habit. The recurring cost buys permissions, accessibility APIs, sandboxing, signing, notarization, OS regressions, updates, backups, and support, not just the visible interface.
xyears of macOS integration, workflow ecosystem, reliability, and community
xyears of OS-version edge cases
xlarge extension ecosystem
xcross-device sync
xcommercial support and rapid compatibility fixes
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Alfred Powerpack pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| single licence | custom | — | 1 user on up to 2 Macs; includes Alfred 5 Powerpack features and Alfred 5 updates. |
| mega supporter | custom | — | 1 user on all personally owned Macs; lifetime upgrades to future Alfred versions. |
| corporate | custom | — | Multiple activations; public numeric price and activation counts are not stated. |
free tierNo free Powerpack tier; the Alfred core app is free and has no published numeric usage cap.
billingone-time licences; no monthly or annual plan
hidden costsThe cheaper Single Licence covers only 2 Macs and excludes future major-version upgrades; Mega Supporter is required for all personal Macs and lifetime upgrades.
verified 2026-08-14 · source ↗
Vibecode Alfred Powerpack
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Alfred Powerpack replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Alfred Powerpack cost?
Alfred Powerpack's pricing is usage-based or varies by plan · Sold as a one-time licence; canonical monthly amount is null..
What do I lose by replacing Alfred Powerpack?
Honestly: years of macOS integration, workflow ecosystem, reliability, and community; years of OS-version edge cases; large extension ecosystem; cross-device sync; commercial support and rapid compatibility fixes. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Alfred Powerpack?
Yes: SuperCmd v1 (The still-maintained open-source v1 has extensions, clipboard, snippets, windows, and bring-your-own AI; v2 is paid.) Raycast Free (Alfred's launcher, snippets, clipboard, and extension store are free; sync and hosted AI are where the invoice starts.) The prompt is for when you want it exactly your way.