Files
lam-agent-vicinae/package.json
T
adrianbonpin 51c16b678f 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.
2026-10-04 15:52:22 +08:00

169 lines
6.3 KiB
JSON

{
"$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json",
"name": "lam-agent",
"title": "Lam Agent",
"description": "Agentic AI chat powered by Ollama. Ask anything from a global hotkey, with streaming answers, persistent sessions, and web search.",
"icon": "extension-icon.png",
"author": "adrianbonpin",
"categories": ["Productivity", "Developer Tools"],
"license": "MIT",
"commands": [
{
"name": "chat",
"title": "Chat with Lam",
"subtitle": "Lam Agent",
"description": "Ask Lam anything. Answers stream in as they are generated, your session history is kept, and Lam can search the web when it needs current information.",
"mode": "view"
},
{
"name": "snip",
"title": "Snip to Lam",
"subtitle": "Lam Agent",
"description": "Capture a region of the screen and start a new chat about it. The launcher window closes first so it cannot end up in the shot. Offered only when the model in preferences supports image input.",
"mode": "no-view"
},
{
"name": "sessions",
"title": "Chat Sessions",
"description": "Browse, reopen, and clean up your past conversations with Lam.",
"mode": "view"
},
{
"name": "setup",
"title": "Set Up Lam Agent",
"description": "Check your Ollama connection, pick the model Lam should use, and tune how Lam answers.",
"mode": "view"
}
],
"preferences": [
{
"name": "apiKey",
"title": "Ollama API Key",
"description": "Your Ollama API key, created at ollama.com/settings/keys. Required to reach cloud models and the web search tools.",
"type": "password",
"required": true
},
{
"name": "model",
"title": "Model",
"description": "The model Lam should use, for example deepseek-v4.1-flash:cloud or gpt-oss:120b. Run \"Set Up Lam Agent\" to pick from the models your key can access.",
"type": "textfield",
"required": true
},
{
"name": "ollamaHost",
"title": "Ollama Host",
"description": "Base URL of the Ollama API. Leave this alone for Ollama Cloud, or point it at http://localhost:11434 to use a local Ollama server.",
"type": "textfield",
"required": false,
"default": "https://ollama.com"
},
{
"name": "systemPrompt",
"title": "System Prompt",
"description": "Extra instructions prepended to every conversation. Leave empty to use Lam's default prompt.",
"type": "textfield",
"required": false
},
{
"name": "location",
"title": "Location",
"description": "The place Lam should treat as yours, for example Cebu City, Philippines. Optional. Without it Lam falls back to your device timezone and locale, which are accurate to a region and not to a city, so anything that depends on exactly where you are (weather, opening hours) is worth filling in.",
"type": "textfield",
"required": false
},
{
"name": "ipGeolocation",
"title": "IP Geolocation",
"label": "Look up my city from my IP address",
"description": "Off by default. When this is on, and the Location preference above is empty, Lam sends your IP address to ipapi.co to work out your city. That is a third party learning roughly where you are, which is why it is opt-in, and why setting Location makes it unnecessary.",
"type": "checkbox",
"required": false,
"default": false
},
{
"name": "webSearchEnabled",
"title": "Web Tools",
"label": "Let Lam search the web",
"description": "Let Lam search the web and fetch pages when a question needs up-to-date information.",
"type": "checkbox",
"required": false,
"default": true
},
{
"name": "thinking",
"title": "Reasoning Effort",
"description": "Ask supported models to reason before answering. Higher effort is slower but more accurate. Ignored by models that do not support it.",
"type": "dropdown",
"required": false,
"default": "off",
"data": [
{ "title": "Off", "value": "off" },
{ "title": "Low", "value": "low" },
{ "title": "Medium", "value": "medium" },
{ "title": "High", "value": "high" }
]
},
{
"name": "showToolCalls",
"title": "Show Tool Calls",
"label": "Show which tools Lam used",
"description": "List the tools Lam called under each answer, for example web_search or get_datetime.",
"type": "checkbox",
"required": false,
"default": true
},
{
"name": "showReasoning",
"title": "Show Reasoning",
"label": "Show the model's reasoning",
"description": "Include the model's thinking trace under each answer. Useful for seeing why a model answered the way it did, but it makes replies much longer.",
"type": "checkbox",
"required": false,
"default": true
},
{
"name": "resumeTimeout",
"title": "Start New Chat After",
"description": "When the command opens, resume the last conversation only if it was used recently. Past this much idle time it starts a fresh chat instead. The old conversation is never deleted — it stays in the sidebar.",
"type": "dropdown",
"required": false,
"default": "1h",
"data": [
{ "title": "Never — always reopen the last chat", "value": "never" },
{ "title": "After 15 minutes", "value": "15m" },
{ "title": "After 1 hour", "value": "1h" },
{ "title": "After 6 hours", "value": "6h" },
{ "title": "After 1 day", "value": "1d" }
]
}
],
"dependencies": {
"@vicinae/api": "^0.29.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "22.13.10",
"@types/react": "19.1.17",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.4.0",
"prettier": "^3.5.3",
"react": "19.1.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.45.0",
"vitest": "^4.1.5"
},
"scripts": {
"dev": "vici develop",
"build": "vici build",
"lint": "vici lint",
"lint:code": "eslint .",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
}
}