Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
a23f54d15f fix: add numbered approval and clarify shortcuts (#956)
All checks were successful
Lint / lint (pull_request) Successful in 21s
2026-04-22 10:44:09 -04:00
Alexander Whitestone
ffb3257cb5 fix: repair auxiliary client syntax corruption 2026-04-22 10:44:00 -04:00
7 changed files with 339 additions and 1810 deletions

View File

@@ -1,4 +1,4 @@
from agent.telemetry_logger import log_token_usage\n"""Shared auxiliary client router for side tasks.
"""Shared auxiliary client router for side tasks.
Provides a single resolution chain so every consumer (context compression,
session search, web extraction, vision analysis, browser vision) picks up
@@ -34,6 +34,8 @@ Payment / credit exhaustion fallback:
their OpenRouter balance but has Codex OAuth or another provider available.
"""
from agent.telemetry_logger import log_token_usage
import json
import logging
import os
@@ -396,7 +398,8 @@ class _CodexCompletionsAdapter:
prompt_tokens=getattr(resp_usage, "input_tokens", 0),
completion_tokens=getattr(resp_usage, "output_tokens", 0),
total_tokens=getattr(resp_usage, "total_tokens", 0),
)\n log_token_usage(usage.prompt_tokens, usage.completion_tokens, model)
)
log_token_usage(usage.prompt_tokens, usage.completion_tokens, model)
except Exception as exc:
logger.debug("Codex auxiliary Responses API call failed: %s", exc)
raise
@@ -529,7 +532,8 @@ class _AnthropicCompletionsAdapter:
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
)\n log_token_usage(usage.prompt_tokens, usage.completion_tokens, model)
)
log_token_usage(usage.prompt_tokens, usage.completion_tokens, model)
choice = SimpleNamespace(
index=0,

240
cli.py
View File

@@ -7254,6 +7254,40 @@ class HermesCLI:
"Use your best judgement to make the choice and proceed."
)
def _handle_clarify_selection(self) -> None:
"""Process the currently selected clarify choice."""
state = self._clarify_state
if not state or self._clarify_freetext:
return
selected = state.get("selected", 0)
choices = state.get("choices") or []
if selected < len(choices):
state["response_queue"].put(choices[selected])
self._clarify_state = None
self._clarify_freetext = False
self._invalidate()
return
if selected == len(choices):
self._clarify_freetext = True
self._invalidate()
def _handle_clarify_number_shortcut(self, number: int) -> bool:
"""Select a clarify option by number key."""
state = self._clarify_state
if not state or self._clarify_freetext:
return False
choices = state.get("choices") or []
max_option = len(choices) + 1
if number < 1 or number > max_option:
return False
state["selected"] = number - 1
self._handle_clarify_selection()
return True
def _sudo_password_callback(self) -> str:
"""
Prompt for sudo password through the prompt_toolkit UI.
@@ -7362,6 +7396,20 @@ class HermesCLI:
choices.append("view")
return choices
def _handle_approval_number_shortcut(self, number: int) -> bool:
"""Select an approval option by number key."""
state = self._approval_state
if not state:
return False
choices = state.get("choices") or []
if number < 1 or number > len(choices):
return False
state["selected"] = number - 1
self._handle_approval_selection()
return True
def _handle_approval_selection(self) -> None:
"""Process the currently selected dangerous-command approval choice."""
state = self._approval_state
@@ -7437,8 +7485,9 @@ class HermesCLI:
preview_lines.extend(_wrap_panel_text(cmd_display, 60))
for i, choice in enumerate(choices):
prefix = ' ' if i == selected else ' '
label = f"{i + 1}. {choice_labels.get(choice, choice)}"
preview_lines.extend(_wrap_panel_text(
f"{prefix}{choice_labels.get(choice, choice)}",
f"{prefix}{label}",
60,
subsequent_indent=" ",
))
@@ -7456,7 +7505,7 @@ class HermesCLI:
_append_panel_line(lines, 'class:approval-border', 'class:approval-cmd', wrapped, box_width)
_append_blank_panel_line(lines, 'class:approval-border', box_width)
for i, choice in enumerate(choices):
label = choice_labels.get(choice, choice)
label = f"{i + 1}. {choice_labels.get(choice, choice)}"
style = 'class:approval-selected' if i == selected else 'class:approval-choice'
prefix = ' ' if i == selected else ' '
for wrapped in _wrap_panel_text(f"{prefix}{label}", inner_text_width, subsequent_indent=" "):
@@ -7465,6 +7514,97 @@ class HermesCLI:
lines.append(('class:approval-border', '' + ('' * box_width) + '\n'))
return lines
def _get_clarify_display_fragments(self):
"""Render the clarify panel for the prompt_toolkit UI."""
state = self._clarify_state
if not state:
return []
def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int:
term_cols = shutil.get_terminal_size((100, 20)).columns
longest = max([len(title)] + [len(line) for line in content_lines] + [min_width - 4])
inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6))
return inner + 2
def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "") -> list[str]:
wrapped = textwrap.wrap(
text,
width=max(8, width),
break_long_words=False,
break_on_hyphens=False,
subsequent_indent=subsequent_indent,
)
return wrapped or [""]
def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None:
inner_width = max(0, box_width - 2)
lines.append((border_style, ""))
lines.append((content_style, text.ljust(inner_width)))
lines.append((border_style, "\n"))
def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None:
lines.append((border_style, "" + (" " * box_width) + "\n"))
question = state["question"]
choices = state.get("choices") or []
selected = state.get("selected", 0)
preview_lines = _wrap_panel_text(question, 60)
for i, choice in enumerate(choices):
prefix = " " if i == selected and not self._clarify_freetext else " "
label = f"{i + 1}. {choice}"
preview_lines.extend(_wrap_panel_text(f"{prefix}{label}", 60, subsequent_indent=" "))
other_number = len(choices) + 1
other_label = (
f" {other_number}. Other (type below)" if self._clarify_freetext
else f" {other_number}. Other (type your answer)" if selected == len(choices)
else f" {other_number}. Other (type your answer)"
)
preview_lines.extend(_wrap_panel_text(other_label, 60, subsequent_indent=" "))
box_width = _panel_box_width("Hermes needs your input", preview_lines)
inner_text_width = max(8, box_width - 2)
lines = []
lines.append(('class:clarify-border', '╭─ '))
lines.append(('class:clarify-title', 'Hermes needs your input'))
lines.append(('class:clarify-border', ' ' + ('' * max(0, box_width - len("Hermes needs your input") - 3)) + '\n'))
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
for wrapped in _wrap_panel_text(question, inner_text_width):
_append_panel_line(lines, 'class:clarify-border', 'class:clarify-question', wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
if self._clarify_freetext and not choices:
guidance = "Type your answer in the prompt below, then press Enter."
for wrapped in _wrap_panel_text(guidance, inner_text_width):
_append_panel_line(lines, 'class:clarify-border', 'class:clarify-choice', wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
if choices:
for i, choice in enumerate(choices):
style = 'class:clarify-selected' if i == selected and not self._clarify_freetext else 'class:clarify-choice'
prefix = ' ' if i == selected and not self._clarify_freetext else ' '
label = f"{i + 1}. {choice}"
wrapped_lines = _wrap_panel_text(f"{prefix}{label}", inner_text_width, subsequent_indent=" ")
for wrapped in wrapped_lines:
_append_panel_line(lines, 'class:clarify-border', style, wrapped, box_width)
other_idx = len(choices)
if selected == other_idx and not self._clarify_freetext:
other_style = 'class:clarify-selected'
other_label = f' {other_number}. Other (type your answer)'
elif self._clarify_freetext:
other_style = 'class:clarify-active-other'
other_label = f' {other_number}. Other (type below)'
else:
other_style = 'class:clarify-choice'
other_label = f' {other_number}. Other (type your answer)'
for wrapped in _wrap_panel_text(other_label, inner_text_width, subsequent_indent=" "):
_append_panel_line(lines, 'class:clarify-border', other_style, wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
lines.append(('class:clarify-border', '' + ('' * box_width) + '\n'))
return lines
def _secret_capture_callback(self, var_name: str, prompt: str, metadata=None) -> dict:
return prompt_for_secret(self, var_name, prompt, metadata)
@@ -8371,17 +8511,8 @@ class HermesCLI:
# --- Clarify choice mode: confirm the highlighted selection ---
if self._clarify_state and not self._clarify_freetext:
state = self._clarify_state
selected = state["selected"]
choices = state.get("choices") or []
if selected < len(choices):
state["response_queue"].put(choices[selected])
self._clarify_state = None
event.app.invalidate()
else:
# "Other" selected → switch to freetext
self._clarify_freetext = True
event.app.invalidate()
self._handle_clarify_selection()
event.app.invalidate()
return
# --- Normal input routing ---
@@ -8501,6 +8632,19 @@ class HermesCLI:
self._approval_state["selected"] = min(max_idx, self._approval_state["selected"] + 1)
event.app.invalidate()
# --- Numbered shortcuts for clarify / approval modal prompts ---
for _digit in '123456789':
@kb.add(_digit, filter=Condition(lambda: bool(self._approval_state) or (bool(self._clarify_state) and not self._clarify_freetext)))
def _handle_modal_number(event, digit=_digit):
number = int(digit)
handled = False
if self._approval_state:
handled = self._handle_approval_number_shortcut(number)
elif self._clarify_state and not self._clarify_freetext:
handled = self._handle_clarify_number_shortcut(number)
if handled:
event.app.invalidate()
# --- /model picker: arrow-key navigation ---
@kb.add('up', filter=Condition(lambda: bool(self._model_picker_state)))
def model_picker_up(event):
@@ -8995,7 +9139,7 @@ class HermesCLI:
if cli_ref._approval_state:
remaining = max(0, int(cli_ref._approval_deadline - _time.monotonic()))
return [
('class:hint', ' ↑/↓ to select, Enter to confirm'),
('class:hint', ' 1-9 or ↑/↓ to select, Enter to confirm'),
('class:clarify-countdown', f' ({remaining}s)'),
]
@@ -9008,7 +9152,7 @@ class HermesCLI:
('class:clarify-countdown', countdown),
]
return [
('class:hint', ' ↑/↓ to select, Enter to confirm'),
('class:hint', ' 1-9 or ↑/↓ to select, Enter to confirm'),
('class:clarify-countdown', countdown),
]
@@ -9086,71 +9230,7 @@ class HermesCLI:
lines.append((border_style, "" + (" " * box_width) + "\n"))
def _get_clarify_display():
"""Build styled text for the clarify question/choices panel."""
state = cli_ref._clarify_state
if not state:
return []
question = state["question"]
choices = state.get("choices") or []
selected = state.get("selected", 0)
preview_lines = _wrap_panel_text(question, 60)
for i, choice in enumerate(choices):
prefix = " " if i == selected and not cli_ref._clarify_freetext else " "
preview_lines.extend(_wrap_panel_text(f"{prefix}{choice}", 60, subsequent_indent=" "))
other_label = (
" Other (type below)" if cli_ref._clarify_freetext
else " Other (type your answer)" if selected == len(choices)
else " Other (type your answer)"
)
preview_lines.extend(_wrap_panel_text(other_label, 60, subsequent_indent=" "))
box_width = _panel_box_width("Hermes needs your input", preview_lines)
inner_text_width = max(8, box_width - 2)
lines = []
# Box top border
lines.append(('class:clarify-border', '╭─ '))
lines.append(('class:clarify-title', 'Hermes needs your input'))
lines.append(('class:clarify-border', ' ' + ('' * max(0, box_width - len("Hermes needs your input") - 3)) + '\n'))
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
# Question text
for wrapped in _wrap_panel_text(question, inner_text_width):
_append_panel_line(lines, 'class:clarify-border', 'class:clarify-question', wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
if cli_ref._clarify_freetext and not choices:
guidance = "Type your answer in the prompt below, then press Enter."
for wrapped in _wrap_panel_text(guidance, inner_text_width):
_append_panel_line(lines, 'class:clarify-border', 'class:clarify-choice', wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
if choices:
# Multiple-choice mode: show selectable options
for i, choice in enumerate(choices):
style = 'class:clarify-selected' if i == selected and not cli_ref._clarify_freetext else 'class:clarify-choice'
prefix = ' ' if i == selected and not cli_ref._clarify_freetext else ' '
wrapped_lines = _wrap_panel_text(f"{prefix}{choice}", inner_text_width, subsequent_indent=" ")
for wrapped in wrapped_lines:
_append_panel_line(lines, 'class:clarify-border', style, wrapped, box_width)
# "Other" option (5th line, only shown when choices exist)
other_idx = len(choices)
if selected == other_idx and not cli_ref._clarify_freetext:
other_style = 'class:clarify-selected'
other_label = ' Other (type your answer)'
elif cli_ref._clarify_freetext:
other_style = 'class:clarify-active-other'
other_label = ' Other (type below)'
else:
other_style = 'class:clarify-choice'
other_label = ' Other (type your answer)'
for wrapped in _wrap_panel_text(other_label, inner_text_width, subsequent_indent=" "):
_append_panel_line(lines, 'class:clarify-border', other_style, wrapped, box_width)
_append_blank_panel_line(lines, 'class:clarify-border', box_width)
lines.append(('class:clarify-border', '' + ('' * box_width) + '\n'))
return lines
return cli_ref._get_clarify_display_fragments()
clarify_widget = ConditionalContainer(
Window(

View File

@@ -1,100 +0,0 @@
# Issue #954 Verification — maps skill guest_house / camp_site / bakery
Status: PASS
## Drift noted
Issue #954 asked for validation on `upstream/main` (commit `c5a814b23`).
Fresh `forge/main` did not contain `skills/productivity/maps/`, so the forge branch was behind upstream for this feature cluster.
This branch ports the upstream maps skill files into the forge checkout and adds regression coverage.
## Automated verification
Command:
```bash
pytest -q tests/skills/test_maps_client.py
```
Result:
- 5 passed
Coverage added:
- maps skill files exist in the repo
- `guest_house` category maps to `tourism=guest_house`
- `camp_site` category maps to `tourism=camp_site`
- `bakery` expands to both `shop=bakery` and `amenity=bakery`
- dual-key bakery results dedupe correctly
- skill documentation lists the new categories and supersedes `find-nearby`
## Manual evidence
### 1) guest_house lookup
Command:
```bash
python3 skills/productivity/maps/scripts/maps_client.py nearby --near "Bath, United Kingdom" --category guest_house --limit 3
```
Observed results:
- Henrietta House — 390.3 m
- The Windsor — 437.2 m
- The Old Rectory Bed & Breakfast — 495.7 m
All returned `tourism=guest_house` in the raw tags.
### 2) camp_site lookup
Command:
```bash
python3 skills/productivity/maps/scripts/maps_client.py nearby --near "Yosemite Valley, California" --category camp_site --limit 5
```
Observed result:
- Yellow Pine Administrative Campground — 90.3 m
Returned `tourism=camp_site` in the raw tags.
### 3) bakery lookup via `shop=bakery`
Command:
```bash
python3 skills/productivity/maps/scripts/maps_client.py nearby --near "Lawrenceville, New Jersey" --category bakery --radius 5000 --limit 10
```
Observed results:
- The Gingered Peach — 713.8 m
- WildFlour Bakery — 741.9 m
Both returned `shop=bakery` in the raw tags.
### 4) bakery lookup via `amenity=bakery`
Command:
```bash
python3 skills/productivity/maps/scripts/maps_client.py nearby --near "20735 Stevens Creek Boulevard, Cupertino, CA" --category bakery --radius 600 --limit 5
```
Observed result:
- Paris Baguette — 28.6 m
Returned `amenity=bakery` in the raw tags (and also includes `shop=bakery`), proving the dual-key union query reaches amenity-tagged bakeries too.
## Conclusion
PASS.
- `guest_house` resolves correctly
- `camp_site` resolves correctly
- `bakery` resolves through both supported keys
- forge/main drift from upstream/main was real and is addressed on this branch

View File

@@ -1,199 +0,0 @@
---
name: maps
description: >
Location intelligence — geocode a place, reverse-geocode coordinates,
find nearby places (46 POI categories), driving/walking/cycling
distance + time, turn-by-turn directions, timezone lookup, bounding
box + area for a named place, and POI search within a rectangle.
Uses OpenStreetMap + Overpass + OSRM. Free, no API key.
version: 1.2.0
author: Mibayy
license: MIT
metadata:
hermes:
tags: [maps, geocoding, places, routing, distance, directions, nearby, location, openstreetmap, nominatim, overpass, osrm]
category: productivity
requires_toolsets: [terminal]
supersedes: [find-nearby]
---
# Maps Skill
Location intelligence using free, open data sources. 8 commands, 44 POI
categories, zero dependencies (Python stdlib only), no API key required.
Data sources: OpenStreetMap/Nominatim, Overpass API, OSRM, TimeAPI.io.
This skill supersedes the old `find-nearby` skill — all of find-nearby's
functionality is covered by the `nearby` command below, with the same
`--near "<place>"` shortcut and multi-category support.
## When to Use
- User sends a Telegram location pin (latitude/longitude in the message) → `nearby`
- User wants coordinates for a place name → `search`
- User has coordinates and wants the address → `reverse`
- User asks for nearby restaurants, hospitals, pharmacies, hotels, etc. → `nearby`
- User wants driving/walking/cycling distance or travel time → `distance`
- User wants turn-by-turn directions between two places → `directions`
- User wants timezone information for a location → `timezone`
- User wants to search for POIs within a geographic area → `area` + `bbox`
## Prerequisites
Python 3.8+ (stdlib only — no pip installs needed).
Script path: `~/.hermes/skills/maps/scripts/maps_client.py`
## Commands
```bash
MAPS=~/.hermes/skills/maps/scripts/maps_client.py
```
### search — Geocode a place name
```bash
python3 $MAPS search "Eiffel Tower"
python3 $MAPS search "1600 Pennsylvania Ave, Washington DC"
```
Returns: lat, lon, display name, type, bounding box, importance score.
### reverse — Coordinates to address
```bash
python3 $MAPS reverse 48.8584 2.2945
```
Returns: full address breakdown (street, city, state, country, postcode).
### nearby — Find places by category
```bash
# By coordinates (from a Telegram location pin, for example)
python3 $MAPS nearby 48.8584 2.2945 restaurant --limit 10
python3 $MAPS nearby 40.7128 -74.0060 hospital --radius 2000
# By address / city / zip / landmark — --near auto-geocodes
python3 $MAPS nearby --near "Times Square, New York" --category cafe
python3 $MAPS nearby --near "90210" --category pharmacy
# Multiple categories merged into one query
python3 $MAPS nearby --near "downtown austin" --category restaurant --category bar --limit 10
```
46 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, guest_house,
camp_site, supermarket, atm, gas_station, parking, museum, park, school,
university, bank, police, fire_station, library, airport, train_station,
bus_stop, church, mosque, synagogue, dentist, doctor, cinema, theatre, gym,
swimming_pool, post_office, convenience_store, bakery, bookshop, laundry,
car_wash, car_rental, bicycle_rental, taxi, veterinary, zoo, playground,
stadium, nightclub.
Each result includes: `name`, `address`, `lat`/`lon`, `distance_m`,
`maps_url` (clickable Google Maps link), `directions_url` (Google Maps
directions from the search point), and promoted tags when available —
`cuisine`, `hours` (opening_hours), `phone`, `website`.
### distance — Travel distance and time
```bash
python3 $MAPS distance "Paris" --to "Lyon"
python3 $MAPS distance "New York" --to "Boston" --mode driving
python3 $MAPS distance "Big Ben" --to "Tower Bridge" --mode walking
```
Modes: driving (default), walking, cycling. Returns road distance, duration,
and straight-line distance for comparison.
### directions — Turn-by-turn navigation
```bash
python3 $MAPS directions "Eiffel Tower" --to "Louvre Museum" --mode walking
python3 $MAPS directions "JFK Airport" --to "Times Square" --mode driving
```
Returns numbered steps with instruction, distance, duration, road name, and
maneuver type (turn, depart, arrive, etc.).
### timezone — Timezone for coordinates
```bash
python3 $MAPS timezone 48.8584 2.2945
python3 $MAPS timezone 35.6762 139.6503
```
Returns timezone name, UTC offset, and current local time.
### area — Bounding box and area for a place
```bash
python3 $MAPS area "Manhattan, New York"
python3 $MAPS area "London"
```
Returns bounding box coordinates, width/height in km, and approximate area.
Useful as input for the bbox command.
### bbox — Search within a bounding box
```bash
python3 $MAPS bbox 40.75 -74.00 40.77 -73.98 restaurant --limit 20
```
Finds POIs within a geographic rectangle. Use `area` first to get the
bounding box coordinates for a named place.
## Working With Telegram Location Pins
When a user sends a location pin, the message contains `latitude:` and
`longitude:` fields. Extract those and pass them straight to `nearby`:
```bash
# User sent a pin at 36.17, -115.14 and asked "find cafes nearby"
python3 $MAPS nearby 36.17 -115.14 cafe --radius 1500
```
Present results as a numbered list with names, distances, and the
`maps_url` field so the user gets a tap-to-open link in chat. For "open
now?" questions, check the `hours` field; if missing or unclear, verify
with `web_search` since OSM hours are community-maintained and not always
current.
## Workflow Examples
**"Find Italian restaurants near the Colosseum":**
1. `nearby --near "Colosseum Rome" --category restaurant --radius 500`
— one command, auto-geocoded
**"What's near this location pin they sent?":**
1. Extract lat/lon from the Telegram message
2. `nearby LAT LON cafe --radius 1500`
**"How do I walk from hotel to conference center?":**
1. `directions "Hotel Name" --to "Conference Center" --mode walking`
**"What restaurants are in downtown Seattle?":**
1. `area "Downtown Seattle"` → get bounding box
2. `bbox S W N E restaurant --limit 30`
## Pitfalls
- Nominatim ToS: max 1 req/s (handled automatically by the script)
- `nearby` requires lat/lon OR `--near "<address>"` — one of the two is needed
- OSRM routing coverage is best for Europe and North America
- Overpass API can be slow during peak hours; the script automatically
falls back between mirrors (overpass-api.de → overpass.kumi.systems)
- `distance` and `directions` use `--to` flag for the destination (not positional)
- If a zip code alone gives ambiguous results globally, include country/state
## Verification
```bash
python3 ~/.hermes/skills/maps/scripts/maps_client.py search "Statue of Liberty"
# Should return lat ~40.689, lon ~-74.044
python3 ~/.hermes/skills/maps/scripts/maps_client.py nearby --near "Times Square" --category restaurant --limit 3
# Should return a list of restaurants within ~500m of Times Square
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
import queue
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock
from cli import HermesCLI
class _FakeBuffer:
def __init__(self, text="", cursor_position=None):
self.text = text
self.cursor_position = len(text) if cursor_position is None else cursor_position
def reset(self, append_to_history=False):
self.text = ""
self.cursor_position = 0
def _make_cli_stub():
cli = HermesCLI.__new__(HermesCLI)
cli._approval_state = None
cli._approval_deadline = 0
cli._approval_lock = threading.Lock()
cli._clarify_state = None
cli._clarify_freetext = False
cli._clarify_deadline = 0
cli._sudo_state = None
cli._sudo_deadline = 0
cli._secret_state = None
cli._secret_deadline = 0
cli._modal_input_snapshot = None
cli._invalidate = MagicMock()
cli._app = SimpleNamespace(invalidate=MagicMock(), current_buffer=_FakeBuffer())
return cli
def test_approval_display_numbers_choices():
cli = _make_cli_stub()
cli._approval_state = {
"command": "sudo rm -rf /tmp/example",
"description": "dangerous command",
"choices": ["once", "session", "always", "deny"],
"selected": 0,
"response_queue": queue.Queue(),
}
rendered = "".join(text for _style, text in cli._get_approval_display_fragments())
assert " 1. Allow once" in rendered
assert "2. Allow for this session" in rendered
assert "3. Add to permanent allowlist" in rendered
assert "4. Deny" in rendered
def test_approval_number_shortcut_submits_choice():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._approval_state = {
"command": "sudo rm -rf /tmp/example",
"description": "dangerous command",
"choices": ["once", "session", "always", "deny"],
"selected": 0,
"response_queue": response_queue,
}
assert cli._handle_approval_number_shortcut(2) is True
assert response_queue.get_nowait() == "session"
assert cli._approval_state is None
def test_approval_selection_still_submits_selected_choice():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._approval_state = {
"command": "sudo rm -rf /tmp/example",
"description": "dangerous command",
"choices": ["once", "session", "always", "deny"],
"selected": 1,
"response_queue": response_queue,
}
cli._handle_approval_selection()
assert response_queue.get_nowait() == "session"
assert cli._approval_state is None
def test_approval_number_shortcut_handles_view_in_place():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._approval_state = {
"command": "sudo dd if=/tmp/in of=/usr/share/keyrings/githubcli-archive-keyring.gpg bs=4M status=progress",
"description": "disk copy",
"choices": ["once", "session", "always", "deny", "view"],
"selected": 0,
"response_queue": response_queue,
}
assert cli._handle_approval_number_shortcut(5) is True
assert cli._approval_state is not None
assert cli._approval_state["show_full"] is True
assert "view" not in cli._approval_state["choices"]
assert cli._approval_state["selected"] == 3
assert response_queue.empty()
def test_clarify_display_numbers_choices_and_other():
cli = _make_cli_stub()
cli._clarify_state = {
"question": "Pick the best option",
"choices": ["Alpha", "Beta", "Gamma", "Delta"],
"selected": 1,
"response_queue": queue.Queue(),
}
rendered = "".join(text for _style, text in cli._get_clarify_display_fragments())
assert "1. Alpha" in rendered
assert " 2. Beta" in rendered
assert "3. Gamma" in rendered
assert "4. Delta" in rendered
assert "5. Other (type your answer)" in rendered
def test_clarify_number_shortcut_submits_choice():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._clarify_state = {
"question": "Pick the best option",
"choices": ["Alpha", "Beta", "Gamma"],
"selected": 0,
"response_queue": response_queue,
}
assert cli._handle_clarify_number_shortcut(3) is True
assert response_queue.get_nowait() == "Gamma"
assert cli._clarify_state is None
assert cli._clarify_freetext is False
def test_clarify_selection_still_submits_selected_choice():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._clarify_state = {
"question": "Pick the best option",
"choices": ["Alpha", "Beta", "Gamma"],
"selected": 1,
"response_queue": response_queue,
}
cli._handle_clarify_selection()
assert response_queue.get_nowait() == "Beta"
assert cli._clarify_state is None
assert cli._clarify_freetext is False
def test_clarify_number_shortcut_activates_other_freetext():
cli = _make_cli_stub()
response_queue = queue.Queue()
cli._clarify_state = {
"question": "Pick the best option",
"choices": ["Alpha", "Beta", "Gamma"],
"selected": 0,
"response_queue": response_queue,
}
assert cli._handle_clarify_number_shortcut(4) is True
assert cli._clarify_state is not None
assert cli._clarify_state["selected"] == 3
assert cli._clarify_freetext is True
assert response_queue.empty()

View File

@@ -1,135 +0,0 @@
"""Regression tests for the bundled maps skill."""
from __future__ import annotations
import importlib.util
from pathlib import Path
from types import SimpleNamespace
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/maps/scripts/maps_client.py"
)
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/maps/SKILL.md"
)
def load_module():
assert SCRIPT_PATH.exists(), f"missing maps client script: {SCRIPT_PATH}"
spec = importlib.util.spec_from_file_location("maps_client_test", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def test_maps_skill_files_exist():
assert SCRIPT_PATH.exists()
assert SKILL_PATH.exists()
def test_category_tags_cover_guest_house_camp_site_and_dual_key_bakery():
module = load_module()
assert module.CATEGORY_TAGS["guest_house"] == ("tourism", "guest_house")
assert module.CATEGORY_TAGS["camp_site"] == ("tourism", "camp_site")
assert module.CATEGORY_TAGS["bakery"] == [
("shop", "bakery"),
("amenity", "bakery"),
]
assert module._tags_for("bakery") == [
("shop", "bakery"),
("amenity", "bakery"),
]
def test_build_overpass_queries_include_all_supported_tags():
module = load_module()
bakery_query = module.build_overpass_nearby(
None,
None,
40.0,
-74.0,
500,
10,
tag_pairs=module._tags_for("bakery"),
)
assert 'node["shop"="bakery"]' in bakery_query
assert 'way["shop"="bakery"]' in bakery_query
assert 'node["amenity"="bakery"]' in bakery_query
assert 'way["amenity"="bakery"]' in bakery_query
guest_house_query = module.build_overpass_nearby(
None,
None,
40.0,
-74.0,
500,
10,
tag_pairs=module._tags_for("guest_house"),
)
assert 'node["tourism"="guest_house"]' in guest_house_query
assert 'way["tourism"="guest_house"]' in guest_house_query
camp_site_bbox = module.build_overpass_bbox(
None,
None,
39.0,
-75.0,
41.0,
-73.0,
10,
tag_pairs=module._tags_for("camp_site"),
)
assert 'node["tourism"="camp_site"]' in camp_site_bbox
assert 'way["tourism"="camp_site"]' in camp_site_bbox
def test_cmd_nearby_dedupes_dual_tag_bakery_results(monkeypatch, capsys):
module = load_module()
duplicate_bakery = {
"elements": [
{
"type": "node",
"id": 101,
"lat": 40.0,
"lon": -74.0,
"tags": {"name": "Wild Flour", "shop": "bakery"},
},
{
"type": "node",
"id": 101,
"lat": 40.0,
"lon": -74.0,
"tags": {"name": "Wild Flour", "amenity": "bakery"},
},
]
}
monkeypatch.setattr(module, "overpass_query", lambda query: duplicate_bakery)
args = SimpleNamespace(
lat="40.0",
lon="-74.0",
near=None,
category="bakery",
category_list=[],
radius=500,
limit=10,
)
module.cmd_nearby(args)
out = capsys.readouterr().out
assert '"count": 1' in out
assert '"Wild Flour"' in out
def test_skill_doc_lists_new_categories_and_supersession():
text = SKILL_PATH.read_text(encoding="utf-8")
assert "guest_house" in text
assert "camp_site" in text
assert "bakery" in text
assert "supersedes: [find-nearby]" in text