Skip to content
yuanyxhPublic

About

A macOS menu-bar toolkit with a built-in local AI layer: MCP memory service, model proxy, daily summary macOS menu-bar toolkit: clipboard, AI translation, OCR, staging, RSS, tasks — plus a local AI layer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Utility

A macOS menu-bar tool: clipboard history, AI translation, OCR capture, drag-and-drop staging, RSS, scheduled tasks, network monitor, plus a local AI layer.

Runs as a regular app by default: Dock icon, main menu, and the terminal window as the main window. Set app.hideDockIcon: true in the config to hide the Dock icon while the terminal window is closed; with the window open the app stays regular so the menu bar and Cmd+Tab keep working.

中文说明


⚠️ Features that are on by default and consume tokens

  • Daily knowledge summary (mcp.summaryEnabled: true, fires at 17:00 by default) — several LLM calls (aggregate, refine, rule extraction)

Turn the relevant switches off in ~/.config/utility/config.yaml if you don't want them.


Install

git clone <https://github.com/yuanyxh/utility.git> utility
cd utility
swift build -c release
.build/release/utility &

Requirements: macOS 14+, Swift 5.9+ (Xcode Command Line Tools).

The AI layer also needs Python 3.12. Its runtime comes from the separate agent-gateway repo:

  • Packaged app: ships the gateway wheel and installs it into ~/.config/utility/mcp-venv on first launch (the dependency step needs the network once; afterwards it runs offline)
  • From source: GatewayResources falls back to a sibling ../agent-gateway/src/agent_gateway, no install needed (a plain swift build ships no wheel — AI features come from that sibling checkout; releases must go through scripts/package-utility.sh); or install manually with python3 -m venv ~/.config/utility/mcp-venv && ~/.config/utility/mcp-venv/bin/pip install -e '../agent-gateway[proxy,memory,server]'

Migrating to another Mac

On the old Mac: Settings (⌘,) → Data & Migration → Export User Data…, pick the categories and save the ZIP. The archive contains preferences, memory, knowledge, clipboard and reminders, terminal workspaces, the notes directory, knowledge cards and RSS output. API keys are stripped unless you enable “Include API keys”.

On the new Mac: the first launch opens the setup wizard. Choose the ZIP, keep each external directory at its original path or choose a new one, and pick skip or overwrite for existing files. The wizard then checks Homebrew, Xcode Command Line Tools, Python 3.12+, tmux, the Python dependencies and the embedding model; missing items are installed after confirmation. Homebrew opens Terminal because its installer needs an administrator password.

Import backs up the current config to ~/.config/utility.backup-<timestamp>/. To roll back:

ditto ~/.config/utility.backup-<timestamp>/ ~/.config/utility/

Paths

Path Default Content Setting
Config dir ~/.config/utility/ all config and data —
Config file ~/.config/utility/config.yaml app configuration —
Python venv ~/.config/utility/mcp-venv/ AI-layer runtime —
Notes dir ~/.config/utility/notes/ sedimented documents; the MCP/ subdir holds Skills, Rules mcp.knowledgeBasePath
Vector store ~/.config/utility/data/qdrant/ hot / cold vectors mcp.qdrantPath
App data ~/.config/utility/data.sqlite clipboard, RSS, … —
Token usage ~/.config/utility/token_usage.sqlite recorded by the proxy —
Tool output ~/.config/utility/tool_output.sqlite full tool-output log —
Model cache ~/.cache/utility/fastembed/ embedding model mcp.fastembedCacheDir
Logs ~/.cache/utility/ per-process logs —
Project graph <project root>/.agent-gateway/graph.sqlite data source for project_lookup / project_impact —
tail -f ~/.cache/utility/debug.log         # runtime trace
tail -f ~/.cache/utility/token_proxy.log   # proxy
tail -f ~/.cache/utility/server.log        # memory service
tail -f ~/.cache/utility/summarize.log     # daily summary
tail -f ~/.cache/utility/llm_client.log    # Python-side LLM calls

Quick start

Hotkey Action
cmd+shift+c open clipboard history
option+d translate the selection
option+s capture a screen region and translate it
drag to the bottom-left corner stage a file, a text selection, or a URL

Architecture

flowchart LR
  subgraph app["Utility.app · Swift/AppKit"]
    direction TB
    ui["menu bar · panels · hotkeys"]
    feat["clipboard · translation · OCR<br/>staging · RSS · tasks · network"]
  end
  subgraph engine["Python AI layer"]
    direction TB
    proxy["model proxy"]
    mem["MCP memory service"]
    sum["daily summary"]
  end
  agentCli(["Agent CLI<br/>Claude Code / any CLI"])

  app <-->|HTTP| engine
  agentCli -->|LLM requests| proxy
  agentCli -->|MCP tool calls| mem
  sum -->|REST| mem
Loading

Other features

Clipboard history

Copied text, files, images and HTML are recorded.

Actions

Action Effect
cmd+shift+c open the panel
↑ / ↓ move the selection
Enter paste into the app you were in and close
Esc close

Settings

Setting Default Purpose
clipboard.maxDays 3 entries older than this many days are deleted
clipboard.excludeApps ["SecurityAgent", "KeychainAccess"] apps never recorded from
clipboard.hotkey cmd+shift+c opens the panel
clipboard.pollInterval 0.3 pasteboard poll interval, seconds

Translation

Requires ai.apiKey.

Actions

Action Effect
option+d translate the selection
cmd+Enter translate what you typed
Enter paste and close
Shift+Enter newline
Esc close

Settings

Setting Default Purpose
translation.hotkey option+d translate the selection
translation.hotkeyOCR option+s capture a screen region, then translate it
translation.targetLang auto starting target language; auto lets the model pick between firstLang and secondLang
translation.firstLang zh one candidate for auto
translation.secondLang en the other
translation.prompt "" inline system prompt overriding the template file; {source}/{target} substituted

OCR capture

Needs Screen Recording permission.

Actions

Action Effect
option+s dim the screen, drag a rectangle, release to recognize and translate into the panel
Esc cancel

Settings

Setting Default Purpose
translation.hotkeyOCR option+s trigger the capture

Drag-and-drop staging

Actions

Action Effect
drag a file / text selection / URL to the bottom-left corner stage it
drag it out of the floating window into any app retrieve it

Settings

Setting Default Purpose
stage.moveOnDragOut true whether dragging out may move the source file: true offers copy+move, false copy only

RSS

Actions

Command Purpose
utility rss --fetch pull new items
utility rss --unread list unread as JSON
utility rss --mark-read <id> mark one read
utility rss --mark-all-read mark everything read
utility rss --digest run the daily digest now (fetch → filter → summarize → archive)
utility rss --dry-run run filtering only, write nothing
utility rss --date YYYY-MM-DD digest date, defaults to today

Settings

Setting Default Purpose
rss.feeds[].name — label in the menu bar
rss.feeds[].url — feed URL
rss:
  feeds:
    - name: "MDN Blog"
      url: "https://developer.mozilla.org/en-US/blog/rss.xml"

Built-in daily digest

With rss.daily.enabled: true the app registers the internal task rss-daily (also triggerable from the menu bar): fetch → history dedup → LLM pre-filter → body fetch and summary → write <outputDir>/YYYY/<quarter>/YYYY-MM-DD.md.

Setting Default Purpose
rss.daily.enabled false register the built-in digest task
rss.daily.cron 0 10 * * * 5-field cron; empty string = manual trigger only
rss.daily.outputDir "" output root; empty = $NOTES_DIR/RSS
rss.daily.markRead true mark processed items read after a successful write
rss.daily.maxConcurrency 4 concurrent body fetches
rss.daily.articleMaxChars 12000 per-article truncation
rss.daily.filterBatch 40 entries per pre-filter call
rss.daily.summaryBatch 4 articles per summary call
rss.daily.maxAgeDays 7 only items published within N days; 0 disables
rss.daily.maxDigestLinks 10 max external links expanded from a weekly digest
rss.daily.fetchTimeout 20 per-page fetch timeout (seconds)
rss.daily.llmTimeout 120 per LLM call timeout (seconds)
rss.daily.clash.enabled false switch a Clash proxy group during fetching, then restore
rss.daily.clash.group / target "" / "" your own Clash group and target node; both must be set or the switch is skipped

Scheduled tasks

Actions

Action Effect
click a task in the menu bar run it
click again (manual mode) kill the running process

Settings

Setting Default Purpose
tasks[].name "" label in the menu bar
tasks[].trigger "" 5-field cron: minute hour day month weekday
tasks[].exec — {type: sh | js | shortcuts | py, command: …}, or a bare string shortcuts://xxx (type inferred)
tasks[].enabled true whether the scheduler registers it
tasks[].mode cron cron runs on schedule and catches up after sleep; manual runs only on menu click, clicking again kills the process
tasks:
  - name: daily report
    trigger: '0 10 * * *'
    exec: { type: sh, command: ~/scripts/daily-report.sh }
    enabled: true
    mode: cron

Knowledge panel

Cards are generated in the background from web sources and shown on a timer. The point is to widen what you run into, not to quiz you on what you already know. The panel follows one topic line, pushing concepts in order; it only shows cards you have never seen unless reviewed cards are enabled.

Each candidate concept passes a scope gate before sources are fetched and the card is written; cards enter the pool in the background, so opening the panel is instant. Cards change on the timer only — the buttons below never advance to another card.

Actions

Action Effect
option+k show or hide the panel
Like mark the card and stay on it; optionally save it as Markdown
Dislike mark the card as disliked and stay on it; the direction feeds back into future concept selection
Copy copy the card as Markdown, sources included

Settings

Setting Default Purpose
knowledge.enabled true master switch
knowledge.refillIntervalMinutes 30 wake-up beat for refilling; one run takes minutes, so keep it larger
knowledge.autoShowEnabled true auto-pop the panel on a timer
knowledge.autoShowIntervalMinutes 120 how often it pops
knowledge.autoCloseSeconds 25 auto-close after this many seconds once the panel loses focus; 0 = never
knowledge.poolTargetTopic 5 refill until this many unseen cards exist
knowledge.maxShownPerCard 2 review cap per card when reviewed cards are enabled
knowledge.maxFailStreak 3 a topic with N dry runs in a row is marked done
knowledge.retentionDays 365 prune cards older than N days on refill; liked cards are kept; 0 = keep forever
knowledge.hotkey option+k shows or hides the panel
knowledge.saveLikedMarkdown true save the current card when Like is clicked
knowledge.likedSaveDir empty Markdown output directory; empty = <mcp.knowledgeBasePath>/知识卡片
knowledge.showReviewedCards false allow old cards after unseen cards run out
knowledge.topics built-in list topic lines (kind: principle or trend; scope / exclude are the hard boundaries of concept selection)

Network monitor

Actions

Action Effect
menu bar shows up/down throughput for active interfaces (0.00M when offline)
Cmd-drag reposition

Terminal workspace

option+space shows or hides the workspace window (change terminal.hotkey; restart required).

Action Effect
Activity bar Switch between the file tree and git status
Single-click a folder Expand / collapse
Single-click a file Open it in the editor on the right
+ in the sub-terminal panel New sub terminal
× on a row Close the session and end its process
Process exit Row is removed
⌘← / ⌘→ Jump to the start / end of the line (⇧ extends)
Drag / wheel Select text; the wheel still reaches TUIs that grab the mouse
Notification OSC 9 / 777 is forwarded as a system notification; click it to open that session
⌘S / ⌘⇧S in the editor Save / save as
Icons at the editor's top-right Toggle Markdown preview / source

A workspace combines a main terminal, sub terminals and a file pane; the split layout is saved as you drag. The file tree follows directory changes and shows new or deleted entries within about 0.4 s; git-ignored entries are dimmed. The active tab, expanded folders, caret and scroll position are restored on restart. The editor provides syntax highlighting, a saved-baseline dirty marker that clears after undo, external-change warnings and crash-draft recovery. The git pane lists staged, unstaged and untracked files with diffs. Markdown and common image files render in place.

The terminal requires tmux (brew install tmux); without it the window notifies you and creates nothing. Sessions keep running after the app quits and reattach on the next open.

terminal.alwaysOnTop: true keeps the window above other apps' windows (useful if clicking the menu bar leaves it covered); it requires a restart.

Menu bar

flowchart TD
  menu["Menu bar"]
  menu --> settings["Settings"]
  menu --> login["Launch at Login"]
  menu --> rss["RSS (3 unread)"]
  rss --> fetch["Fetch RSS"]
  rss --> read["Mark All Read"]
  menu --> mcp["● MCP"]
  mcp --> status["Running"]
  mcp --> restart["Restart Service"]
  mcp --> path["Data Path"]
  menu --> tasks["Tasks (2)"]
  tasks --> run["Click to Run"]
  menu --> clear["Clear Clipboard"]
  menu --> quit["Quit ⌘Q"]
Loading

The AI Agent layer

Component Responsibility
Model proxy reverse-proxy LLM requests: routing, protocol conversion, usage recording, token governance
MCP memory service expose memory and codebase search as MCP tools
Daily summary sediment memories into notes
flowchart LR
  agent(["Agent"])
  subgraph P["Proxy layer"]
    direction LR
    p1["forward"] --> p2["record"] --> p3["inject"] --> p4["govern"] --> p5["…extendable"]
  end
  subgraph M["Memory / knowledge base"]
    direction LR
    m1["memory · knowledge base"] --> m2["produce"] --> m3["sediment"] --> m4["consume"]
    m4 --> m2
    m4 -.-> m5["…experience summary"]
  end
  agent --> p1
  agent --> m1
Loading

1. Model proxy

Reverse-proxies Anthropic / OpenAI protocol requests, with model switching, token governance and usage recording.

Enable

modelProxy:
  enabled: true
  port: 0
  providers:
    - name: example
      url: "https://api.example.com/anthropic"
      apiFormat: anthropic
      apiKey: "xxxx-xxxx-xxxx-xxxx"
      models: [your-model]

Set the agent's base URL to http://localhost:[PORT]/open/v1. /open/v1 is a compatibility alias, /v1 is the protocol-native entry. /raw/v1 is the transparent entry: same provider routing, but no rule injection, skill dedup, output trimming or response annotation — use it for one session when you need an unmodified agent run. It is opt-in per session via the base URL and does not affect other sessions.

Routing

flowchart TD
  req["request body.model"] --> match{"matches a provider's models?"}
  match -->|yes| hit["route to that provider<br/>upstream model = original"]
  match -->|no| miss["fall back to providers[0]<br/>upstream model = models[0]"]
Loading

Token governance

Mechanism Toggle Effect
Tool-output de-dup toolOutputDedup a short note replaces byte-identical tool results
Shell output trimming outputFilter.enabled shell output filtered, JSON losslessly minified
Classifier acceleration clientAccel disables thinking for safety-classifier requests

De-dup: when a tool result is byte-identical to one already sent, replace the content with a short note:

[Tool_Name] result already exists; see above

Shell trimming: oversized output truncated head/tail (keep 70% + 30%) with a self-labeling marker:

[... N bytes truncated ...]

Settings

Setting Default Purpose
modelProxy.enabled false start the proxy with the app
modelProxy.port 0 listening port; 0 auto-picks a free one (the [PORT] in the URLs above)
modelProxy.url "" single upstream, used only when providers is empty
modelProxy.providers[].name — label in the menu bar
modelProxy.providers[].url — upstream endpoint
modelProxy.providers[].apiFormat — anthropic or openai; differing from the client's protocol converts both ways
modelProxy.providers[].apiKey "" key substituted when forwarding; empty passes the client's key through
modelProxy.providers[].models — model IDs routed to this provider
modelProxy.toolOutputDedup true collapse byte-identical tool results
modelProxy.outputFilter.enabled true trim oversized shell output
modelProxy.outputFilter.maxBytes 12000 only outputs above this size are trimmed
modelProxy.responseAnnotationEnabled true append model | rewrote: … to final replies
modelProxy.contextWarnTokens 300000 context size above which the appended line suggests a new session (0 = never)
modelProxy.clientAccel {enabled: true, clients: []} clients that get classifier acceleration
modelProxy.clientText — client-injected continuation/summary text and tags
modelProxy.maxRestarts / restartDelayMs 3 / 2000 restart policy after a crash

2. MCP memory service

Exposes MCP tools over streamable HTTP at http://127.0.0.1:[PORT]. Register it with an MCP client:

claude mcp add --transport http mino-brain http://127.0.0.1:[PORT]

Three-layer memory

Layer Storage Content Lifecycle
hot mem0 + Qdrant (local) raw fragment facts hotRetentionDays (default 180 days)
cold Qdrant collection knowledge_base structured knowledge entries long-lived
notes .md/.txt under the notes dir sedimented documents permanent

Tools

Tool Purpose
remember / remember_batch write memories (fact + context, optional importance/protected)
recall three-layer joint retrieval
memory_get full body and lifecycle state for any id, including archived entries
memory_graph follow one edge type (version chain, episode, sources, disputes)
catch_up pull recent memories
forget delete a memory
set_memory_processed read/write the processed bit
project_lookup locate a symbol or file in the dependency graph
project_impact callers/callees of a symbol, multi-hop
project_notes search project-local Markdown docs
tool_resource read a skill's companion resources
websearch_engine multi-engine search (Google via offscreen WKWebView) with optional page Markdown
read_url fetch a URL as Markdown; local-only unless use_proxy=true
(dynamic) each skill under MCP/Skills registers as a tool

The Google branch shells out to utility websearch --query <q> [--limit N], which renders the results page in an offscreen WKWebView and prints one JSON line.

Notes directory

Configured by mcp.knowledgeBasePath, default ~/.config/utility/notes/:

Path Meaning
MCP/Skills/ each .md or directory containing SKILL.md registers as an MCP tool
MCP/Rules/ rules / conventions the daily summary distilled from memories
MCP/System_Prompts.md rules the proxy injects into each turn's user message
Extracts/ intermediate extraction products of the daily summary
other .md / dirs sedimented knowledge articles

Skills

Each .md under <notes dir>/MCP/Skills/ (or a SKILL.md inside such a directory) registers as an MCP tool; - in the name normalizes to _. Frontmatter:

---
name: my-skill
description: when it should trigger and what it does; the model uses this to decide when to call it
---

The body returned when the tool is called.

Add enable: false to skip registration without deleting the file; omitted or any other value keeps it registered.

A directory skill may carry companion resources (references/, agents/, scripts/ are read via tool_resource and are not registered as tools):

MCP/Skills/my-skill/
├── SKILL.md
├── references/
├── agents/
└── scripts/

System_Prompts.md

Plain text, no format requirement; injected verbatim into each turn's user message:

## Rules
- Always answer in Chinese
- Move deleted files to the Trash instead of removing them

Rules

norms_<level>.md (e.g. norms_global, norms_backend), distilled and maintained by the daily summary; each file's own frontmatter registers it as a tool:

---
name: norms_global
description: fetch this behavioral spec before acting; global general conventions.
---

# Global conventions

## Entries

### Verify the current code state before reporting a feature's status
- domain: collaboration
- norm: ...
- source: ...

Extracts

Intermediate extraction products of the daily summary, grouped into topic directories, generated automatically.

Example configuration

examples/ ships complete, ready-to-use examples, one set per language:

Path Contents
examples/zh-Hans/MCP/ Chinese
examples/en/MCP/ English

Each set contains System_Prompts.md and 6 skills (check / health / hunt / skill-creator / think / ui). Web search and URL reading are built-in tools (websearch_engine / read_url) and need no skill. To use one, copy it into the notes directory's MCP/:

cp -R examples/en/MCP/* ~/.config/utility/notes/MCP/

Rules/ (maintained automatically by the daily summary) are not included.

Restart the service after adding or changing a skill.

Settings

Setting Default Purpose
mcp.enabled true start the service with the app
mcp.port 0 listening port; 0 auto-picks a free one (the [PORT] in the URLs above)
mcp.hotRetentionDays 180 how long raw fragments stay in the hot layer
mcp.archiveDays 90 age at which memories move to the cold layer
mcp.summaryEnabled true run the daily summary pipeline
mcp.summaryHour 17 hour of day it runs
mcp.excludeTags — memories carrying these tags skip the knowledge-base pipeline
mcp.memoryProtectImportance 0 memories with importance (1–10) at or above this are never archived or superseded; 0 disables
mcp.memoryMaxContentChars 0 soft length limit per memory in characters, warns but never rejects; 0 unlimited
mcp.memoryMaxSummaries / memoryMaxSummariesPerEpisode 0 budgets for active consolidation summaries; over budget the oldest fold into one higher-level summary (max 3 levels); 0 disables folding
mcp.rankSignalRecency / rankSignalAccess / rankSignalQuality 0 optional ranking signals; benchmark each on the evaluator set before enabling, all 0 keeps plain score order
mcp.synonyms {} search synonyms, an increment over the built-in table
mcp.toolDefaults e.g. recall: 6 default result caps per tool
mcp.qdrantPath / knowledgeBasePath "" storage paths; empty uses the default paths
mcp.noteExcludeDirs [] note directories skipped by recall/summary; empty skips none, non-empty fully overrides
mcp.noteExtensions [] note file extensions; empty uses the built-in generic set, non-empty fully overrides
mcp.embedderModel "" embedding model; empty uses sentence-transformers/all-MiniLM-L6-v2
mcp.embedderIdleUnloadSeconds 600 release the embedding model after this many idle seconds; 0 keeps it resident
mcp.fastembedCacheDir "" model cache; empty uses ~/.cache/utility/fastembed/
mcp.maxRestarts / restartDelayMs 3 / 2000 restart policy after a crash
search.runtime timeoutSeconds: 25 cascade hard timeout; optional parallel / concurrency

Restart from menu bar → MCP after changing any of these.


3. Daily summary

Fired daily at mcp.summaryHour (default 17:00) by the TaskScheduler; sediments hot-layer memories into notes and syncs into the cold layer.

flowchart LR
    A["Aggregate<br/>cluster the last 3 days of memories by topic, dedupe"] --> B["Refine<br/>extract keywords → search similar topics<br/>decide merge / new → generate body"] --> C["Rules<br/>rule-like memories → MCP/Rules/*.md"]
Loading

Written articles go into the notes directory, then an automatic git commit runs. Log: ~/.cache/utility/summarize.log.


4. Prompt system

Each LLM call site's system / user text is not hardcoded but externalized to prompts/<lang>/<feature>.md in the agent-gateway repo, one file each for system and user.

Call site feature Purpose
Daily summary · value judgement quality_judge / quality_judge_user decide whether a memory is worth sedimenting
Daily summary · aggregate aggregate / merge_cross_batch cluster by topic, merge and dedupe within and across batches
Daily summary · refine extract_keywords / decide_merge / gen_new / gen_merge extract keywords, decide merge or create, generate the body
Daily summary · rule extraction extract_rules write rule memories to MCP/Rules/*.md
MCP memory service contradiction detect contradictions against similar memories on write
Menu-bar translation translate / translate_auto translate the selection; translate_auto detects the direction

Lookup order: <config dir>/prompts/<feature>.md (user override) → built-in Gateway/prompts/<lang>/<feature>.md (by language, falling back to zh-Hans).

The front-matter's name must match the filename; placeholders and the body's actual placeholders are cross-validated.


5. Timed reminders

utility notice writes reminders to ~/.config/utility/notices.sqlite; the running Utility.app polls for due rows every 5 minutes, posts a system notification, then deletes the row. The CLI is a short-lived process with no notification authorization, so it only registers.

Command Effect
utility notice add --in 10m "title" "body" relative delay (units s/m/h/d)
utility notice add --at 20:30 "title" absolute time, rolls to tomorrow when past
utility notice add --at "2026-10-29 09:00" "title" explicit date and time
utility notice list list pending reminders
utility notice cancel <id> cancel a reminder

Reminders do not fire while the app is not running; on launch it replays those within 24 hours and collapses older ones into a single "missed reminders" notice. There is no repeat mode — use a cron task for anything periodic.


6. Project index and Agent parasitic mode

The data behind project_lookup / project_impact lives in .agent-gateway/graph.sqlite under the project root. The gateway owns scanning and writes (sync reconciles the tree on start; FileWatcher mirrors file events incrementally), while GraphScanner only sends protocol requests. The graph stores files, symbols, call/type/import edges and unresolved references.

Command Purpose
utility -a "<agent command>" [--dir <dir>] Agent parasitic mode: build the graph, start the file watcher, launch the Agent

What utility -a does:

flowchart LR
    A["build or sync graph.sqlite"] --> B["start FSEvents watcher"] --> C["spawn the Agent CLI<br/>stdin / stdout / stderr passthrough"] --> D["Agent exits → stop watcher → exit"]
Loading

Environment injected into the Agent subprocess: MCP_PORT / MINO_BRAIN_MCP_PORT (MCP service port).

utility -a "claude -p 'analyze this module'"
utility --dir ~/myapp -a claude

Configuration

Config lives in ~/.config/utility/config.yaml, written with defaults on first launch. Settings → Open Config edits it in place; ⌘↩ validates and saves (nothing is written when validation fails). Reload in the settings window rereads the file from disk and applies it at runtime. Changes to mcp.* or modelProxy.* need the matching service restarted from the menu bar.

Global

Setting Default Purpose
general.language "" UI language; empty follows the system. Also selects the prompt set and the MCP tool-argument text
general.proxyEnabled false use the macOS system proxy settings
general.httpTimeout 30 HTTP request timeout, seconds
general.httpRetryCount 0 HTTP retry count
general.aiTimeout 120 agent subprocess timeout, seconds; 0 = unlimited
general.logMaxSize 10485760 max size of one log file, bytes
general.logBackupCount 3 rotated log files kept
notifications.enabled true master switch for system notifications

AI provider

Setting Default Purpose
ai.baseURL "" OpenAI-compatible endpoint host (e.g. https://api.example.com)
ai.apiKey "" API key; translation and the summary pipeline are unavailable without it
ai.model "" model name, e.g. your-model
ai.llmTimeout 120 Python-side single LLM inference timeout, seconds; 0 or negative falls back to the default
# Example provider
ai: { baseURL: "https://api.example.com", apiKey: "sk-your-key", model: "your-model" }

# OpenAI
ai: { baseURL: "https://api.openai.com", apiKey: "sk-your-key", model: "gpt-4o-mini" }

# local Ollama
ai: { baseURL: "http://localhost:11434", apiKey: "ollama", model: "llama3.2" }

mcp.* and modelProxy.* are listed in their sections above.


Packaging

SIGNING_IDENTITY=<cert name or SHA1> ./scripts/package-utility.sh

Five steps: clean build → sign the binary → assemble Utility.app → sign the bundle (with a hard codesign --verify) → produce Utility-<version>.dmg.

Choosing a certificate

SIGNING_IDENTITY accepts either the certificate name or its SHA1 hash; look it up with security find-identity -v -p codesigning.

Use Certificate Notes
Local use only self-signed certificate generated locally, no Apple involvement; an app signed this way is blocked by Gatekeeper on other Macs and cannot be notarized
Distribution Developer ID Application issued by Apple, can be notarized; requires an Apple Developer Program membership

A self-signed certificate is generated locally: Keychain Access → Certificate Assistant → Create a Certificate… (type "Code Signing", self-signed), or generate one with openssl and import it into the login keychain.

Getting a Developer ID certificate

Requires an Apple Developer Program membership ($99/year). Only the Account Holder can create Developer ID certificates, up to 5.

Option A — Xcode. Xcode → Settings → Accounts → add your Apple ID → select your team → Manage Certificates… → + → Developer ID Application.

Option B — manual CSR. Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority, save the .certSigningRequest. On developer.apple.com → Certificates → + → Developer ID Application → upload the CSR → download the .cer → double-click to install into the login keychain.

Confirm it's usable:

security find-identity -v -p codesigning
# "Developer ID Application: Your Name (TEAMID)"  — the TEAMID is what you need

Don't use the "Developer ID Application Managed" certificate — its private key lives on Apple's servers and it can't be used from the command line.

Notarization

An un-notarized app is blocked by Gatekeeper on other machines. Notarize and staple the .dmg:

# store credentials once (app-specific password from appleid.apple.com)
xcrun notarytool store-credentials "utility-notary" \
  --apple-id "you@example.com" --team-id "TEAMID" --password "xxxx-xxxx-xxxx-xxxx"

xcrun notarytool submit Utility-<version>.dmg --keychain-profile utility-notary --wait
xcrun stapler staple Utility-<version>.dmg

Troubleshooting

Symptom Likely cause Fix
Memory service crashes or won't start Python venv missing or broken Recreate it (see Install); check ~/.cache/utility/server.log
Proxy unreachable (ECONNREFUSED) Proxy disabled, or wrong base URL Set modelProxy.enabled: true; confirm ANTHROPIC_BASE_URL points at the proxy port
OCR selects but returns nothing Screen Recording not granted System Settings → Privacy & Security → Screen Recording → enable Utility, restart
Hotkeys do nothing Accessibility not granted, or the combo is taken Grant Accessibility, or rebind
Gateway lists no third-party models The client filters discovery to claude* Type the model directly: /model your-model
Memory service alive but requests time out A stale process holds the port Restart from the menu bar; lsof -nP -iTCP -sTCP:LISTEN | grep [PORT]
Upstream 502 out of nowhere Proxy/upstream compression mismatch Ensure the venv has Brotli/backports.zstd, restart the proxy
tail -f ~/.cache/utility/debug.log
sqlite3 ~/.config/utility/data.sqlite ".tables"

Tech stack

Layer Choice
Language Swift 5.9+ / Python 3.12
UI AppKit (menu bar + NSPanel)
Storage GRDB (SQLite) + Qdrant (vector DB)
Config Yams (YAML)
Hotkeys Carbon EventHotKey
OCR Vision (VNRecognizeTextRequest)
Proxy aiohttp reverse proxy
MCP protocol mcp.server + StreamableHTTPServerTransport
Memory engine Mem0 (hot) + fastembed + Qdrant (cold)
Project index AST + regex + FTS5 multi-language symbol extraction
Network getifaddrs
Login item SMAppService
CLI swift-argument-parser

License

MIT — see LICENSE.

About

A macOS menu-bar toolkit with a built-in local AI layer: MCP memory service, model proxy, daily summary macOS menu-bar toolkit: clipboard, AI translation, OCR, staging, RSS, tasks — plus a local AI layer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages