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.yamlif you don't want them.
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-venvon first launch (the dependency step needs the network once; afterwards it runs offline) - From source:
GatewayResourcesfalls back to a sibling../agent-gateway/src/agent_gateway, no install needed (a plainswift buildships no wheel — AI features come from that sibling checkout; releases must go throughscripts/package-utility.sh); or install manually withpython3 -m venv ~/.config/utility/mcp-venv && ~/.config/utility/mcp-venv/bin/pip install -e '../agent-gateway[proxy,memory,server]'
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/| 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| 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 |
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
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 |
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 |
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 |
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 |
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 |
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: cronCards 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) |
Actions
| Action | Effect |
|---|---|
| menu bar | shows up/down throughput for active interfaces (0.00M when offline) |
Cmd-drag |
reposition |
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.
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"]
| 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
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]"]
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 |
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 themRules
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.
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"]
Written articles go into the notes directory, then an automatic git commit runs. Log: ~/.cache/utility/summarize.log.
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.
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.
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"]
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 claudeConfig 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.
| 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 |
| 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.
SIGNING_IDENTITY=<cert name or SHA1> ./scripts/package-utility.shFive steps: clean build → sign the binary → assemble Utility.app → sign the bundle (with a hard codesign --verify) → produce Utility-<version>.dmg.
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.
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 needDon'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.
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| 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"| 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 |
MIT — see LICENSE.