Claude
Skills
Sign in
Back

lil-agents-macos-dock

Included with Lifetime
$97 forever

```markdown

Writing & Docs

What this skill does

```markdown
---
name: lil-agents-macos-dock
description: Tiny AI companions (Bruce and Jazz) that live on your macOS dock and provide Claude Code, OpenAI Codex, and GitHub Copilot CLI access via animated characters
triggers:
  - "add lil agents to my dock"
  - "set up dock AI companions"
  - "configure lil agents"
  - "add a new character or theme to lil agents"
  - "integrate claude codex copilot with lil agents"
  - "build lil agents from source"
  - "customize lil agents appearance"
  - "troubleshoot lil agents not showing"
---

# lil-agents macOS Dock AI Companions

> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.

lil-agents places animated AI companion characters (Bruce and Jazz) above your macOS dock. Click a character to open a themed terminal popover that shells out to your chosen AI CLI (Claude Code, OpenAI Codex, or GitHub Copilot). Characters walk, display thinking bubbles, and play sound effects — all rendered from transparent HEVC video bundled in the app.

---

## What It Does

| Feature | Detail |
|---|---|
| Characters | Bruce & Jazz walk back and forth above the dock |
| AI backends | Claude Code, OpenAI Codex CLI, GitHub Copilot CLI |
| Themes | Peach, Midnight, Cloud, Moss |
| Terminal | Themed popover with live streaming output |
| Thinking bubbles | Playful phrases while the agent runs |
| Updates | Sparkle framework for auto-updates |
| Privacy | Fully local — no telemetry, no accounts |

---

## Requirements

- macOS Sonoma 14.0+
- Xcode 15+ (to build from source)
- At least one AI CLI installed

### Install AI CLIs

```bash
# Claude Code
# Download from https://claude.ai/download and install

# OpenAI Codex
npm install -g @openai/codex

# GitHub Copilot CLI
brew install copilot-cli
```

---

## Building from Source

```bash
git clone https://github.com/ryanstephen/lil-agents.git
cd lil-agents
open lil-agents.xcodeproj
# Press ⌘R in Xcode to build and run
```

No additional package manager steps are required — dependencies (Sparkle) are resolved automatically by Xcode's Swift Package Manager integration.

---

## Project Structure

```
lil-agents/
├── lil-agents.xcodeproj/
├── lil agents/
│   ├── App/
│   │   ├── AppDelegate.swift          # NSApplication entry, StatusItem, Sparkle
│   │   └── OnboardingWindowController.swift
│   ├── Characters/
│   │   ├── CharacterWindowController.swift   # Transparent overlay window above dock
│   │   ├── CharacterView.swift               # AVPlayer HEVC rendering
│   │   └── ThinkingBubbleView.swift
│   ├── Terminal/
│   │   ├── TerminalPopoverController.swift   # Popover terminal UI
│   │   ├── AgentProcess.swift                # Shells out to CLI
│   │   └── TerminalTheme.swift               # Peach/Midnight/Cloud/Moss
│   ├── Settings/
│   │   └── SettingsManager.swift             # UserDefaults-backed config
│   ├── Resources/
│   │   ├── bruce/                    # HEVC .mov files (walk, think, idle)
│   │   ├── jazz/                     # HEVC .mov files
│   │   └── Sounds/
│   └── Info.plist
└── README.md
```

---

## Core Architecture

### Transparent Window Above the Dock

Characters live in a `NSWindow` with `level = .statusBar` positioned just above the dock frame.

```swift
// CharacterWindowController.swift pattern
import AppKit
import AVKit

class CharacterWindowController: NSWindowController {

    private var playerView: AVPlayerView!
    private var player: AVPlayer!

    override func windowDidLoad() {
        super.windowDidLoad()

        guard let window = window else { return }

        // Make window transparent and click-through by default
        window.isOpaque = false
        window.backgroundColor = .clear
        window.hasShadow = false
        window.ignoresMouseEvents = false   // false so clicks register
        window.level = .statusBar           // float above normal windows

        // Position above the dock
        positionAboveDock()
        setupHEVCPlayer()
    }

    private func positionAboveDock() {
        guard let screen = NSScreen.main else { return }
        let dockHeight = getDockHeight(for: screen)
        let charSize = CGSize(width: 80, height: 80)

        // Start at left edge, above dock
        let origin = CGPoint(
            x: 100,
            y: dockHeight + 4
        )
        window?.setFrame(CGRect(origin: origin, size: charSize), display: true)
    }

    private func getDockHeight(for screen: NSScreen) -> CGFloat {
        // visibleFrame excludes dock and menu bar
        let visible = screen.visibleFrame
        let full    = screen.frame
        return visible.minY - full.minY   // bottom inset = dock height
    }

    private func setupHEVCPlayer() {
        guard let url = Bundle.main.url(forResource: "bruce-walk",
                                        withExtension: "mov") else { return }
        player = AVPlayer(url: url)
        player.actionAtItemEnd = .none  // we loop manually

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(playerDidReachEnd),
            name: .AVPlayerItemDidPlayToEndTime,
            object: player.currentItem
        )

        playerView = AVPlayerView(frame: window!.contentView!.bounds)
        playerView.player = player
        playerView.videoGravity = .resizeAspect
        playerView.controlsStyle = .none
        window?.contentView?.addSubview(playerView)
        player.play()
    }

    @objc private func playerDidReachEnd(_ notification: Notification) {
        player.seek(to: .zero)
        player.play()
    }
}
```

### Shelling Out to AI CLIs

```swift
// AgentProcess.swift pattern
import Foundation

enum AIBackend: String, CaseIterable {
    case claude  = "claude"
    case codex   = "codex"
    case copilot = "gh"     // `gh copilot suggest`

    var executablePath: String {
        // Resolve from common install locations
        let candidates: [String]
        switch self {
        case .claude:
            candidates = ["/usr/local/bin/claude", "/opt/homebrew/bin/claude"]
        case .codex:
            candidates = ["/usr/local/bin/codex", "/opt/homebrew/bin/codex",
                          "\(NSHomeDirectory())/.npm-global/bin/codex"]
        case .copilot:
            candidates = ["/usr/local/bin/gh", "/opt/homebrew/bin/gh"]
        }
        return candidates.first { FileManager.default.fileExists(atPath: $0) }
               ?? "/usr/local/bin/\(rawValue)"
    }

    func buildArguments(for prompt: String) -> [String] {
        switch self {
        case .claude:
            return ["-p", prompt]
        case .codex:
            return ["-p", prompt]
        case .copilot:
            return ["copilot", "suggest", "-t", "shell", prompt]
        }
    }
}

class AgentProcess {

    var onOutput: ((String) -> Void)?
    var onComplete: (() -> Void)?
    var onError: ((String) -> Void)?

    private var process: Process?

    func run(prompt: String, backend: AIBackend) {
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: backend.executablePath)
        proc.arguments = backend.buildArguments(for: prompt)

        // Inherit a PATH that includes Homebrew and npm globals
        var env = ProcessInfo.processInfo.environment
        env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:"
                    + "\(NSHomeDirectory())/.npm-global/bin:"
                    + (env["PATH"] ?? "")
        proc.environment = env

        let pipe = Pipe()
        proc.standardOutput = pipe
        proc.standardError  = pipe

        pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
            let data = handle.availableData
            guard !data.isEmpty,
                  let text = String(data: data, encoding: .utf8) else { return }
            DispatchQueue.main.async { self?.onOutput?(text) }
        }

        proc.terminationHandler = { [weak self] _ in
            DispatchQueue.main.async { self?.onComplete?() }
        }

        do {
      

Related in Writing & Docs