---
title: CLI Control (control Omniscio from scripts and hotkeys) (part 3)
---

# CLI Control (control Omniscio from scripts and hotkeys) (part 3)

## What it is

This is part 3 of the [CLI Control (control Omniscio from scripts and hotkeys)](cli-control.md) page. It covers the endpoints an outside AI uses to *look at* and drive what is already in front of you — your coaching interviews and artifacts, your bookmarks, inbox alerts, an actual picture of the window, an on-screen spotlight it can point with, and a jump straight to one setting.

## Where to find it

Same surface once more — one local address, one token from **Settings → CLI Control**. What changes here is what is being pointed at: these endpoints reach features you already know in the app, so an AI showing you a bookmark, dropping an alert in your inbox, reading a coaching artifact or circling a control on screen is touching the same thing you would click yourself.

## How it behaves

### AI Coaching — `/ai-coaching/*`

The `/ai-coaching/*` routes expose the AI Coaching virtual project — interview prompts, saved artifacts and their version history, the core profile and pinned context, session bookmarks, and compaction summaries — for external automation. Reads are bearer-gated and read-budgeted at 60/min per token-hash. Writes split two ways: **approval-gated** artifact and interview mutations (`POST /ai-coaching/artifacts` → `ai_coaching.artifact_create`, `PATCH`/`DELETE /ai-coaching/artifacts/:id`, `POST /ai-coaching/interviews` → `ai_coaching.start_interview`) land as `pending` rows in the same `cli_pending_actions` queue as cron/automation/away-mode/recipes, while the bookmark mutations (`POST`/`PATCH`/`DELETE /ai-coaching/bookmarks`) and the session-scoped `POST /ai-coaching/sessions/:sessionId/artifact` — the in-app coach's own title-locked save, confined to its own session — apply **immediately**. A kill switch at Settings → CLI Control (`aiCoachingCliEnabled`, default `true`) flips every route to 403 with `disabled: true` when off. PATCH carries optimistic concurrency via `expectedLatestVersion` (returns `STALE_ARTIFACT` on mismatch). Full request/response shapes, `curl` examples, and the approval-gated pipeline: [cli-ai-coaching.md](cli-ai-coaching.md).

### Bookmarks — `/bookmarks/*`

Six routes (`GET /bookmarks`, `POST /bookmarks`, `PATCH /bookmarks/:id`, `DELETE /bookmarks/:id`, `POST /bookmarks/reorder`, `POST /bookmarks/:id/launch`) expose the bookmarks feature for external automation. Mutations apply **immediately** (like keybindings) — they're personal launcher entries with no destructive cascade — and emit `bookmarks:changed` push so any open Omniscio UI re-renders. Two security rules apply: **`exe`-kind launches are rejected at the CLI surface (403)**, and **`cmd` first-run still requires user confirmation in the Omniscio popover** — the CLI launch schema is `z.object({}).strict()` so a CLI caller cannot bypass via `confirmed: true`. A kill switch at Settings → CLI Control → Bookmarks API (`bookmarksCliEnabled`, default `true`) flips every route to 403 when off. Read endpoint is bearer-gated and read-budgeted at 60/min per token-hash. Full request/response shapes, `curl` examples, and the reorder all-or-nothing contract: [bookmarks.md](bookmarks.md).

### Inbox alerts — `/alert`

Seven routes (`POST /alert`, `GET /alert`, `GET /alert/:id`, `POST /alert/:id/archive`, `POST /alert/:id/unarchive`, `DELETE /alert/:id`, `POST /alert/:id/stuck-task-helper`) let any session or agent **drop a persistent row in the user's inbox** — a build broke, a PR is ready, a long job finished. Creating one applies **immediately** (no approval round-trip — a benign, reversible attention row, unlike the billable spawn routes), so it's the one agent-facing way to proactively surface something. A kill switch at Settings → Features → **Allow agents to raise inbox alerts** (`agentAlertsEnabled`, default **on**) flips `POST /alert` to 403 when off; the read / archive / unarchive / delete routes are never gated. The seventh route — `POST /alert/:id/stuck-task-helper` (accept a "want a hand getting unstuck?" offer carried by the alert) — is the exception: it is **approval-gated and billable** because accepting it spawns a paid coach session, so it enqueues a `stuck_task.start_helper` approval (idempotent via `X-Client-Request-Id`) rather than applying immediately. The create body is `title` plus a `contentType` of `text` / `link` / `file` and its one matching content field (`text`, or `fileBase64` + `fileName`); add an optional `dedupKey` to coalesce repeats into one ×N row, or a `projectId` to also surface it in that project's **Needs You** section. Mutations share the 10/min bucket; reads the 60/min budget. Full request/response shapes, the ~750 KB file ceiling, dedup, and inbox behaviour: [inbox-alerts.md](inbox-alerts.md).

```bash
TOKEN=$([ -n "$AMC_CLOUD_SESSION_BOX" ] && printf '%s' "$AMC_CLI_TOKEN" || cat ~/.amc/cli-token)

# Drop a text alert in the inbox (applies immediately — no approval round-trip)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -d '{"title":"Build failed on main","contentType":"text","text":"The 14:02 CI run failed — needs a look.","dedupKey":"ci-main-fail"}' \
  http://127.0.0.1:19519/alert
```

### UI Highlight, Discovery + Screenshot — `/ui/highlight`, `/ui/snapshot`, `/ui/screenshot`

Six routes (`GET /ui/snapshot`, `GET /ui/screenshot`, `POST /ui/highlight`, `POST /ui/highlight/clear`, `GET /ui/highlight/status`, `POST /ui/open`) let an external AI **see what's on screen**, **point the user at it**, **know when the user acts**, and **open a feature surface**. `/ui/snapshot` is the agent's structural **eyes** (a JSON list of anchored elements + positions); `/ui/screenshot` is its **camera** (an actual PNG of the window, for when text isn't enough); `/ui/highlight` is its **finger** (an ad-hoc spotlight + tooltip on one or more DOM elements — _"open this menu"_, _"flip this toggle"_, _"the X you're asking about lives here"_); `/ui/highlight/status` is its **ears** — it reports back when the user actually clicks a highlighted element, so the agent can drive a genuinely **interactive, click-to-advance** walkthrough (see the target → wait for the click → react / draw the next step). Use a single highlight step for one-off pointing; use a **multi-step flow** when the user needs Next/Previous navigation between several places; add `advanceOn` to a step to make it wait for the user's real click instead of a Next button.

**Anchor-only contract.** `/ui/snapshot` emits ONLY elements that carry `data-ui-anchor="<stable-name>"` — a curated registry of UI surface that the AI is allowed to point at. There is no auto-selector synthesis, no text/aria scraping, no element cap. If an element doesn't have an anchor, the AI can't see it. The trade-off is intentional: the agent gets a small, stable, human-labelled list (with `description`, `when`, `related`) instead of a 300-element auto-derived blob whose selectors break on every layout shuffle. `blindSpots` tells the agent how many interactives are out there but unreachable, so an answer like _"that button doesn't have an anchor yet — file an issue"_ is grounded.

- `GET /ui/snapshot` (auth, **`200 OK`**) — walk the live DOM in the renderer for `[data-ui-anchor]` elements and return a JSON snapshot. Response shape: `{ ok: true, snapshot: { route, viewport, capturedAt, blindSpots, elements: [...] } }`. Each element entry has `{ anchor, selector, label, kind, rect, visible, value?, description?, when?, related? }`. **`selector` is always `[data-ui-anchor="<name>"]`** — the only stable selector shape. **`label`** comes from the registry's `description` or the element's `aria-label` (no fallback to text content). **`kind`** comes from the registry (`button`, `link`, `input`, `toggle`, `tab`, `menuitem`, `row`, `region`, `composite`). **`description` / `when` / `related`** are human-written metadata from `src/shared/ui-anchor-registry.ts` — the agent uses these to choose which anchor to highlight. **`blindSpots: { unanchored, shadowDom, webview }`** counts interactives (buttons, links, inputs, role=button) that are visible + in-viewport but lack `data-ui-anchor`, so the agent knows how much of the surface is currently un-pointable. No element cap — the registry is curated and small by construction.
- `GET /ui/screenshot` (auth, **`200 OK`**) — capture the renderer window as a PNG. By default returns **raw `image/png` bytes** (so `curl -o screen.png` saves it and the agent can read the file); pass `?format=base64` for a JSON body `{ ok: true, format: "png", width, height, dataBase64 }` when the caller can't consume binary. The image is downscaled so its long edge is ≤ **1568 px** (the threshold AI vision pipelines downscale to anyway — bounds payload + token cost with no detail loss). Uses Electron's `webContents.capturePage()`, which captures ONLY this window's own pixels (not the desktop, not other apps). Errors: **401** (missing/invalid token), **429** (own 30/min budget drained), **503** (`{ ok: false, error }` — window missing/destroyed/minimized or a blank frame; bring Omniscio to the foreground and retry), **500** (capture/encode failed). Pairs with `/ui/snapshot`: snapshot tells you what's there and where; screenshot shows you what it looks like. This is also how the in-app **Ask Omniscio** agent sees the screen (it holds the same bearer token).
- `POST /ui/highlight` (auth, **`200 OK`**) — show the overlay. Two body shapes (both `.strict()`):
  - **Single-step (shorthand)**: `{ selector, title?, message?, mode?, autoDismissMs?, requestId? }`. The route normalizes this to a one-element steps array before emitting.
  - **Multi-step**: `{ steps: [{ selector, title?, message? }, ...], startIndex?, mode?, autoDismissMs?, requestId? }`. `steps` is 1–20 entries; `startIndex` is clamped to a valid index. The renderer shows Previous · _"Step N of M"_ · Next/Finish in the footer; Arrow Left / Arrow Right also navigate. Steps with unresolvable selectors auto-advance forward (and back) so a stale anchor in the middle doesn't trap the user.
  - Each step's `selector` is any CSS selector `document.querySelector` understands (1–500 chars; canonical is `[data-ui-anchor="…"]` from `/ui/snapshot` but `#id` / `[aria-label="…"]` / `.class` still work for ad-hoc cases). `title` (≤80 chars) renders in the tooltip header — defaults to **"AI Tip"** when omitted. `message` (≤2000 chars) renders in the body. **Both `title` and `message` accept Markdown** (bold, italics, inline code, links, lists) — rendered with the same `ProseBlock` pipeline as agent chat messages. `mode` is reserved for future variants and currently only accepts `"spotlight"`. `autoDismissMs` (500–60 000) overrides the default; pass `null` to disable auto-dismiss entirely. Defaults: **single-step auto-dismisses after 8 s; multi-step does NOT auto-dismiss** (stays until the user finishes / dismisses) — multi-step flows can have unbounded reading + clicking time. `requestId` (≤64 chars) is echoed in not-found logs so a caller can correlate which highlight failed. **Response shape**: `{ ok: true, found: boolean[], steps: number }` — `found[i]` is whether step `i`'s selector resolves to a **visible** element (real on-screen layout size) at request time, NOT mere existence. An off-screen-but-sized element is `found: true` (the overlay scrolls it into view) — EXCEPT one inside the conversation message scroller (`[data-scroll-container]`), which can't be scrolled into view there (raw scrolling fights the chat scroll engine) and so is `found: false`, matching what the overlay actually shows; a `0×0` / `display:none` match is `found: false` because highlighting it would dim the screen with no visible cutout. So treat `found: false` as _"this highlight won't actually show — pick a different, visible target"_ rather than retrying the same selector. The route still emits the push event even when one or more steps return `found: false` (the renderer's per-step retry loop catches late-rendering targets and the auto-advance handles permanent misses).
  - **Interactive (click-to-advance) steps.** Any step may carry `advanceOn` to make the overlay **wait for the user to act**, then record it and advance — instead of the read-and-Next default. `advanceOn` is a discriminated union: `{"type":"click"}` (the user clicks the highlighted `selector`) or `{"type":"anchor-appears","anchor":"<name>"}` (a named `data-ui-anchor` element mounts — use this to confirm an _outcome_, e.g. a success toast, without intercepting a click). A `click` step also honors **`suppressAction`** (boolean, click-steps only): omitted/`false` = **REAL** — the overlay is click-through, so the click performs the real action (really opens the menu, really flips the toggle) and the tour advances; `true` = **PRETEND** — a transparent click-catcher detects the click but suppresses the real handler, a safe demo that changes nothing. The mode is per-step, so one flow can mix real and pretend steps. Each acted step is recorded to the renderer's event bucket for `/ui/highlight/status` to report. `advanceOn` composes with multi-step footers (the user can still use Next/Previous), and an interactive single-step flow does **not** auto-dismiss (it waits for the act).
- `POST /ui/highlight/clear` (auth, **`200 OK`**) — dismiss any active overlay. Body `{ requestId? }` (`.strict()`). Idempotent — succeeds with 200 even if no overlay is showing.
- `GET /ui/highlight/status` (auth, **`200 OK`**) — _"has the user acted yet?"_ Returns the interactive-step outcomes the overlay recorded (from `advanceOn` steps). Query params (both optional): `requestId` filters to one highlight's events (pass the same `requestId` you sent to `/ui/highlight`); `waitMs` (0–25 000) turns the call into a bounded **long-poll** — the route re-checks every 250 ms and returns as soon as an event arrives or the deadline passes, so an agent can BLOCK on the user's click without busy-polling. **Response shape**: `{ ok: true, clicked: boolean, events: [...] }` — `clicked` is `true` when at least one matching event exists; each event is `{ requestId?, stepIndex, selector, type: "click" | "anchor-appears", mode: "real" | "pretend", at }` (`at` = epoch ms). The event bucket is cleared each time a new highlight is shown, so a stale click from a prior highlight never leaks in (scope with `requestId` for belt-and-suspenders). Read-only — never spawns, spends, or mutates. Errors: **401** (missing/invalid token), **429** (own 120/min budget drained), **503** (`{ ok: false, error }` — main window missing/destroyed).
- `POST /ui/open` (auth, **`200 OK`**) — open a feature surface in the renderer, the CLI twin of the per-feature open hotkeys. Body `{ target }` (`.strict()`) where `target` is one of `mindmap`, `whiteboard`, `scratchpad`, `alarm`, `meetings`, `super-prompts`, `question-widget`, `ask` (ask-about-this-page popover), `quick-replies`, `help` (help & docs panel), `tag-palette` (tag picker). Apply-immediately, shares the 10/min mutation bucket, purely visual — it emits the same push the open hotkey fires (so each surface's own feature gate + layout logic stays single-sourced), can't spawn / spend / destroy. A `200` means the open was accepted (a feature-gated target no-ops; `meetings` / `question-widget` toggle rather than always open, matching the hotkey).

- `POST /ui/deep-target` (auth, **`200 OK`**) — **navigate to a view AND spotlight a control in one call.** Body: `{ path, anchor, title?, message?, autoDismissMs?, requestId? }` (`.strict()`). `path` is a navigate-only deep-link path string — same namespace as `omniscio://` routes (e.g. `setting/cli-control`, `project/Foo`, `session/<id>`, `note/<vault-path>`) — and `anchor` is a `data-ui-anchor` name (lowercase alphanumeric + hyphens; invalid charset returns `400`). Spawn paths and any non-navigation action kind return `400`. After navigating, the renderer waits (bounded ~6 s, MutationObserver) for the anchor to mount, then spotlights it with the same overlay as `POST /ui/highlight`. An agent call drops a click-to-go toast (same agent-foreground guard as all other view-switch routes); the spotlight spec is carried inside the toast and replays when the user clicks through. Gated by the `deep-target-highlight` unreleased feature (`settingKey: deepTargetHighlightEnabled`): returns `403` when the feature is off. Shares the 10/min mutation pool. For a shareable / clickable version of the same operation, append `?highlight=<anchor>[&highlightTitle=&highlightMessage=]` to any navigate-only `omniscio://` deep link instead of calling this route.

**Visual.** The spotlight uses a thickened **3 px** blue ring with an outer halo glow and a gentle 2-second pulse animation so small toolbar icons are obvious — it's deliberately more prominent than the app-tour spotlight (which uses a thin 2 px ring). The tooltip has a header bar with the title on the left and the X dismiss button on the right (X stays in the header, not the body, so the rich-text body uses the full width). Multi-step flows add a footer row with Previous · _"Step N of M"_ · Next/Finish.

**Snapshot transport — dead-reliable two-path.** `/ui/snapshot` first tries `mainWindow.webContents.executeJavaScript('window.__amcCollectUiSnapshot()')` — the **fast path**, a renderer bridge installed at **preload time** (via `contextBridge.exposeInMainWorld`, not from a React component) so the bridge is available the moment the renderer process starts. If the fast path returns `null` (bridge not yet installed) or throws (renderer crashed mid-collect), the route falls back to **inline injection**: a self-contained collector script with the `STATIC_UI_ANCHORS` registry baked in as a JSON literal, executed via the same `executeJavaScript` channel. The inline collector walks the DOM identically to the preload bridge and returns the same shape. Either path can succeed independently — `/ui/snapshot` only returns 503 when BOTH return `null` (the main window is destroyed or the renderer is totally unresponsive). The request is bounded by a 3 s timeout per path so a hung renderer can never pin the request thread. **Single-flight lock**: concurrent callers share one in-flight Promise. The practical result: an external AI session can call `/ui/snapshot` against any build (dev, packaged, sandbox) at any time without first verifying the preload bridge is current.

**Highlight transport.** `/ui/highlight` and `/ui/highlight/clear` are fire-and-forget on the renderer side — they emit a push event (`UI_HIGHLIGHT_SHOW` / `UI_HIGHLIGHT_HIDE`) and return immediately. `/ui/highlight` also probes every step's selector synchronously in the renderer first so it can report the per-step `found[]` array in the response. The renderer overlay resolves the selector for the active step, retries 5×30 ms (~150 ms) for late-rendering targets, and renders the overlay. It only ever targets a **visible** element: it prefers an in-viewport match, else scrolls an off-screen-but-sized match into view first (EXCEPT inside the conversation message scroller, where raw scrolling would fight the chat scroll engine), and NEVER resolves a zero-size element — a `0×0` rect would paint a full-screen dim with a collapsed, invisible cutout ("screen dims, card shows, nothing highlighted"). **Selector matches nothing visible** → the renderer logs a `warn`-level entry to `<userData>/logs/main.log` (with `selector` + `requestId`); in multi-step flows the active step auto-advances forward (or back, if you came from the right) to the next resolvable step; in single-step flows the overlay silently dismisses. **Single-instance** — a new SHOW replaces any active overlay. **User dismissal paths**: × button in the header, Escape key, click anywhere outside the tooltip (capture phase, doesn't block the underlying click — clicks INSIDE the tooltip on Next/Previous never dismiss), the auto-dismiss timer (single-step only by default), or `POST /ui/highlight/clear`.

No setting kill-switch — the overlay is purely visual, can't spawn processes, can't spend money, and can't perform destructive actions. Disabling means rotating the bearer token via Omniscio → Settings → CLI Control → Regenerate. **Rate limits**: `/ui/highlight` and `/ui/highlight/clear` share the global 10/minute mutation pool. `/ui/snapshot` has its **own** 60/minute read budget so a polling agent can refresh every second without burning the mutation pool that writers depend on (drained budget returns `429 { ok: false, error: "Rate limit exceeded (60 snapshots/minute)", retryAfter: 60 }`). `/ui/screenshot` has its **own** 30/minute read budget (lower than snapshot because a PNG is a much heavier payload), also separate from the mutation pool. `/ui/highlight/status` has its **own** 120/minute read budget (generous because it's a poll / long-poll endpoint), also separate from the mutation pool. The raw-PNG response carries the same security-header floor (`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, CSP, `Referrer-Policy: no-referrer`) as the JSON responses. Bearer-token auth required (no query-string `?token=`).

```bash
TOKEN=$([ -n "$AMC_CLOUD_SESSION_BOX" ] && printf '%s' "$AMC_CLI_TOKEN" || cat ~/.amc/cli-token)

# 1) Discover what's on screen — agent reads this first to choose a selector
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:19519/ui/snapshot
# {"ok":true,"snapshot":{
#   "route":"#/settings","viewport":{"w":1280,"h":800,"scrollY":0},
#   "capturedAt":"2026-05-15T07:00:00.000Z",
#   "blindSpots":{"unanchored":12,"shadowDom":0,"webview":0},
#   "elements":[
#     {"anchor":"app-inbox-button","selector":"[data-ui-anchor=\"app-inbox-button\"]",
#      "label":"Go to inbox","kind":"button","rect":{"x":16,"y":12,"w":24,"h":24},
#      "visible":true,"description":"Top-left Omniscio logo button — jumps to the unified inbox.",
#      "when":"Always present in the title bar.",
#      "related":["app-attention-count"]},
#     ...
#   ]
# }}

# 1b) Grab an actual picture of the window, then read it
curl -sH "Authorization: Bearer $TOKEN" http://127.0.0.1:19519/ui/screenshot -o /tmp/amc-screen.png
# (now open /tmp/amc-screen.png — e.g. an agent reads the image file)
# JSON variant for clients that can't save binary:
curl -sH "Authorization: Bearer $TOKEN" "http://127.0.0.1:19519/ui/screenshot?format=base64"
# {"ok":true,"format":"png","width":1280,"height":800,"dataBase64":"iVBORw0KG..."}

# 2) Single-step highlight with Markdown body (default 8s auto-dismiss)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"selector":"[data-ui-anchor=\"app-inbox-button\"]","title":"**Your inbox**","message":"Click here to triage *cross-project* alerts.\n\nKeyboard shortcut: `Ctrl+Q`"}' \
  http://127.0.0.1:19519/ui/highlight
# {"ok":true,"found":[true],"steps":1}

# 3) Multi-step flow — guided sequence with Next/Previous (no auto-dismiss)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "steps": [
      {"selector":"[data-ui-anchor=\"app-inbox-button\"]","title":"Step 1","message":"This is your **inbox** — every alert lands here."},
      {"selector":"[data-ui-anchor=\"app-focus-pill\"]","title":"Step 2","message":"This is **focus mode** — batches notifications so you only get pinged on a threshold."},
      {"selector":"[data-ui-anchor=\"app-settings-button\"]","title":"Step 3","message":"Open **Settings** to configure rules and thresholds."}
    ]
  }' \
  http://127.0.0.1:19519/ui/highlight
# {"ok":true,"found":[true,true,true],"steps":3}

# 4) Stale selector — emits anyway so renderer can log + auto-advance in multi-step
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"selector":"[data-ui-anchor=\"removed-anchor\"]","message":"…"}' \
  http://127.0.0.1:19519/ui/highlight
# {"ok":true,"found":[false],"steps":1}

# 5) Interactive (click-to-advance) — point at a real control, then WAIT for the user to click it
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"selector":"[data-ui-anchor=\"app-settings-button\"]","title":"Open Settings","message":"Click the gear to open Settings — I'\''ll continue once you do.","advanceOn":{"type":"click"},"requestId":"tour-1"}' \
  http://127.0.0.1:19519/ui/highlight
# {"ok":true,"found":[true],"steps":1}

# ...then long-poll for the click (blocks up to 20s, returns the instant they click)
curl -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/ui/highlight/status?requestId=tour-1&waitMs=20000"
# {"ok":true,"clicked":true,"events":[{"requestId":"tour-1","stepIndex":0,
#   "selector":"[data-ui-anchor=\"app-settings-button\"]","type":"click","mode":"real","at":1750000000000}]}

# Safe DEMO variant — suppressAction detects the click but suppresses the real action (nothing opens)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"selector":"[data-ui-anchor=\"app-settings-button\"]","message":"Try clicking it — this is just a demo, nothing will actually happen.","advanceOn":{"type":"click"},"suppressAction":true}' \
  http://127.0.0.1:19519/ui/highlight
# {"ok":true,"found":[true],"steps":1}

# Manually dismiss
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{}' \
  http://127.0.0.1:19519/ui/highlight/clear
```

**Dev tool.** `npm run ui-snapshot` (or `npm run ui-snapshot:summary` for a one-line-per-anchor overview) hits `/ui/snapshot` against the running app and pretty-prints the JSON — useful when wiring new `data-ui-anchor` attributes. See [`scripts/dump-ui-snapshot.js`](/scripts/dump-ui-snapshot.js).

**Adding an anchor.** New trainable element? Add `data-ui-anchor="<kebab-name>"` to its JSX, then add a matching entry to your feature's co-located `*.ui-anchors.ts` file with `{ description, kind, when?, related? }` and run `npm run ui-anchors:reindex` (per `ui-anchor-colocation-contract.md`) — `src/shared/ui-anchor-registry.ts` is now a generated re-export barrel; never edit it or the generated file directly. Internal-only anchors used by tests/keyboard nav (not user-trainable) get `internal: true` and are stripped from the snapshot output. The collector is the only consumer of the registry, so no other wiring is needed.

### Navigate to a setting — `GET /settings/open`

Take the user straight to a specific setting from a script or an AI. `GET /settings/open?q=<query>` (auth, **`200 OK`**) emits the same `open-setting` deep link the in-app Settings-search "jump to this setting" uses: Omniscio opens Settings, jumps to the matching section, scrolls to the row, and flashes it. `q` is **plain English** — it resolves through the SAME fuzzy catalog the Settings search box uses (labels + keywords + synonyms), so `q=backup%20mirror`, `q=spend%20cap`, and `q=default%20model` each land on the right row. Read-only and free — it navigates, changes nothing.

Like the other view-switch routes, an **agent-initiated** `/settings/open` (one sending `X-AMC-Source-Session-Id`) no longer jumps your view directly: while you're in Omniscio it drops a **click-to-go toast** and returns `{ ok: true, action: "open-setting", suggested: true, reason: "nav-suggested" }` (the pane opens only when you click the toast); while a different app owns the foreground it drops an inbox note and returns `{ ok: true, action: "open-setting", focusSuppressed: true, reason: "user-in-another-app" }`. Opt in at Settings → CLI Control → "Let agents switch your view directly" (default off) to restore an immediate jump to the matched pane. It's the CLI twin of the `agentmc://setting/<query>` deep link.

Pair it with the read/patch settings routes: `GET /settings/open` NAVIGATES the user to a setting, `GET /settings/:key` READS a value, and `PATCH /settings/:key` CHANGES one (approval-gated — see the omniscio-control **settings** surface). `GET /settings/search?q=` returns the matching setting **candidates** (via a renderer round-trip, so the app window must be open) — use it to inspect what would match, then `/settings/open` to take the user there. And `GET /deep-link/resolve?url=` dry-runs any `omniscio://` link, returning the action it resolves to (or a "did you mean") without navigating — for a `setting` link it also confirms the setting really exists, so one that would dead-end comes back as a failure naming the nearest real settings instead of a false green (with the app closed it says the setting could not be verified rather than guessing either way). A setting link may name its target by the **AppSettings key** these very routes use (`gitGuardrailsReadyTagGateEnabled`), a setting id, a section id, or a phrase.

```bash
TOKEN=$([ -n "$AMC_CLOUD_SESSION_BOX" ] && printf '%s' "$AMC_CLI_TOKEN" || cat ~/.amc/cli-token)

# Jump the user straight to the Backup Mirror setting
curl -sH "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/settings/open?q=backup%20mirror"
# {"ok":true,"action":"open-setting","query":"backup mirror"}
```

## Related

The endpoints that change projects, sessions, docs, away-mode rules and recipes are in [part 2](cli-control-part-2.md), and switching the server on plus calling it from a script is on the [parent page](cli-control.md). The coaching data these routes expose is described in [cli-ai-coaching.md](cli-ai-coaching.md), the alert surface in [inbox-alerts.md](inbox-alerts.md), and the launcher behind the bookmarks routes in [bookmarks.md](bookmarks.md).
