feat: opt-in IP geolocation for get_location

Adds the one thing the extension could not answer before: roughly where
the user actually is, rather than which zone their clock is set to.

- `lib/geo.ts` resolves a place from ipapi.co. Chosen because it is HTTPS
  and needs no API key; ip-api.com, the other obvious free option, is
  HTTP-only on its free tier, which is not a place to send an IP address.
- Off by default, and skipped entirely when the user has set Location: the
  better answer is already there, so spending their privacy on a worse one
  would be indefensible. `ipPlace` is only wired into ToolContext when the
  preference is on, so an off setting cannot reach the network at all.
- Cached for 6 hours. A city does not change between questions, and every
  lookup hands the provider another copy of the address. Set Up Lam Agent
  clears it on re-test, alongside the vision cache.
- Failures are returned, not swallowed: a rate limit or an offline machine
  says so in the tool output while the region answer stays available. The
  provider signals some failures with error: true and HTTP 200, so the body
  is checked as well as the status.
- The tool output always names the provider and calls the result city-level
  rather than exact. It also flags an IP timezone that disagrees with the
  device clock, which usually means a VPN.
- The README claimed no geolocation service ever saw the user's address.
  That is now conditional, so the section was rewritten rather than left to
  quietly become untrue.

Deliberately not done: GPS. CoreLocation has no CLI on macOS and GeoClue's
usual provider resolves nearby Wi-Fi through a Mozilla service anyway.

Verified: 341 tests (25 new), tsc, eslint, prettier, and vici build. I did
not make a live API call, because doing so would send this machine's IP to
the provider as the exact flow being opted into.
This commit is contained in:
2026-10-04 15:52:22 +08:00
parent 400f81fa6c
commit 51c16b678f
10 changed files with 604 additions and 13 deletions
+12 -2
View File
@@ -122,6 +122,8 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
| **Ollama API Key** | None | Required. Stored by Vicinae as a password preference. |
| **Model** | None | Required. Any model your key can reach. |
| **Ollama Host** | `https://ollama.com` | Set to `http://localhost:11434` for a local server. |
| **Location** | None | The place Lam should treat as yours, for example `Cebu City, Philippines`. Optional. The only exact location available, and the only one that stops anything being inferred. |
| **IP Geolocation** | off | Off by default. When on, and only when **Location** is empty, your IP is sent to ipapi.co to resolve a rough city. |
| **System Prompt** | Lam's default | Replaces the built-in prompt entirely. The default asks for plain prose: no em dashes, no generated-text tells, no restating the question. |
| **Web Tools** | on | Enables `web_search` and `web_fetch`. |
| **Reasoning Effort** | Off | `low`/`medium`/`high` for models that support thinking. |
@@ -136,11 +138,19 @@ Lam calls these on its own when they would improve an answer. All four are read-
| Tool | What it does |
| --- | --- |
| `get_datetime` | Current local date, time, weekday, timezone, and UTC offset. |
| `get_location` | Timezone, UTC offset, locale, region, and city. |
| `get_location` | The place you configured, or a rough city from your IP if you enabled that, plus timezone, UTC offset, locale and region. |
| `web_search` | Searches the web; returns titles, URLs, and snippets. |
| `web_fetch` | Fetches one page and returns its content as markdown. |
`get_location` is derived from your device timezone rather than GPS or an IP lookup, so no third-party geolocation service sees your address. Treat it as "which region am I in", not "where am I".
`get_location` reports the **Location** you set, if you set one, because that is the only answer here that is actually about where you are. Everything else is inferred from your device, and the inference is coarser than it looks.
**A timezone is not a city.** This is the trap the tool used to fall into: it reported `City: Manila`, taken from the last segment of the IANA zone name. That name is the database's representative place, not yours, and a single zone is often a whole country. `Asia/Manila` is the Philippines, `Asia/Kolkata` is India, `Europe/Oslo` is Norway, `Australia/Brisbane` is Queensland. Reporting the zone's namesake as your city was wrong for anyone outside it, and confidently so, because a model asked about the weather will use it without hesitating. The zone's label is still reported, as `Timezone area`, and named for what it is.
The **region** comes from your locale, which is a declaration rather than an inference: a locale of `en-PH` says the Philippines because that is what you set. It costs nothing and it cannot be wrong the way a zone name can.
If you turn on **IP Geolocation**, and only then, `get_location` resolves a rough city from your IP address via [ipapi.co](https://ipapi.co). That is a third party learning approximately where you are, so it is off by default, it is skipped entirely whenever you have set a Location (the better answer is already there, and there is no reason to spend your privacy on a worse one), and the tool output names the provider and says the result is city-level rather than exact. The answer is cached for 6 hours rather than looked up per question, because a city does not change between questions and every lookup hands over another copy of your address. `Set Up Lam Agent` clears both that cache and the capability cache when you re-test.
This extension does not use GPS. That would mean CoreLocation on macOS, which has no command-line interface, or GeoClue on Linux, whose usual provider resolves nearby Wi-Fi networks through a Mozilla service anyway. Neither is worth it for "is it late where you are".
Web search results and fetched pages can be large. Lam truncates them before they reach the model, and gives up after 5 rounds of tool calls rather than looping.