{
  "schema": "nlo.agent-tools.v0",
  "status": "preview: not switched on at neverlogoff.com yet",
  "note": "Link hub tools for a person's own AI: links, short-link counts, other accounts, the page's look. Same format as agents/tools-pages.json. Same key as the page (typ nlo-page-push, made at /me/page). ask_human_first = get the human's yes for this exact call. Anything that would show something on the public page answers 409 needs_confirm: ask the human, then repeat the call with confirm_public: true. Guide: agents/hub.md.",
  "base_url": "https://neverlogoff.com",
  "auth_kinds": {
    "none": "no sign-in",
    "cookie": "the human's sign-in cookie, only in their browser",
    "page_token": "Authorization: Bearer nlou1.… with typ nlo-page-push (the page key). 60 minutes. Reads and changes THIS person's page and hub only."
  },
  "tools": [
    {
      "name": "get_hub",
      "description": "Read the hub: the page address, every link (with its short link and whether it shows right now), the connected accounts (with verified true/false and shown/hidden), the look, the theme list and the icon names.",
      "inputSchema": { "type": "object", "additionalProperties": false, "properties": {} },
      "annotations": { "readOnlyHint": true },
      "x_nlo": { "method": "GET", "op": "me", "path": "/api/hub?op=me", "auth": "page_token", "ask_human_first": false, "status": "preview" }
    },
    {
      "name": "add_link",
      "description": "Add one link and save. https:// only. Each link gets a short link /s/<code> that counts clicks per day. icon: leave empty to pick from the site, or one of the icon names from get_hub. show_from/show_until: ISO date-times. A link visitors would see answers needs_confirm first.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["title", "url"], "properties": { "title": { "type": "string", "maxLength": 80 }, "url": { "type": "string", "pattern": "^https://", "maxLength": 500 }, "icon": { "type": "string" }, "featured": { "type": "boolean" }, "visibility": { "enum": ["public", "followers", "approved", "only_me"] }, "show_from": { "type": "string", "format": "date-time" }, "show_until": { "type": "string", "format": "date-time" }, "at": { "type": "integer", "minimum": 0 }, "confirm_public": { "type": "boolean", "description": "only after the human said yes" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "add-link", "path": "/api/hub?op=add-link", "auth": "page_token", "ask_human_first": true, "status": "preview", "needs_confirm": "409 {error:'needs_confirm', links:[id]}" }
    },
    {
      "name": "update_link",
      "description": "Change one link (any of title, url, icon, featured, visibility, show_from, show_until). Changing what a public link says or where it goes, or turning a link public, answers needs_confirm first.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["id"], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9]{2,12}$" }, "title": { "type": "string" }, "url": { "type": "string", "pattern": "^https://" }, "icon": { "type": "string" }, "featured": { "type": "boolean" }, "visibility": { "enum": ["public", "followers", "approved", "only_me"] }, "show_from": { "type": "string" }, "show_until": { "type": "string" }, "confirm_public": { "type": "boolean" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "update-link", "path": "/api/hub?op=update-link", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    },
    {
      "name": "delete_link",
      "description": "Delete one link. Its short link stops working (answers 404). Its past counts stay in the stats.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["id"], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9]{2,12}$" } } },
      "annotations": { "readOnlyHint": false, "destructiveHint": true },
      "x_nlo": { "method": "POST", "op": "delete-link", "path": "/api/hub?op=delete-link", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    },
    {
      "name": "reorder_links",
      "description": "Put the links in a new order. Send every link id exactly once, top first.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["order"], "properties": { "order": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9]{2,12}$" } } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "reorder-links", "path": "/api/hub?op=reorder-links", "auth": "page_token", "ask_human_first": false, "status": "preview" }
    },
    {
      "name": "import_links",
      "description": "Bring links over from another link page. YOU fetch the page (on the human's computer) and parse it — hub-import.js / hub-import.js does both — then send only {links:[{title,url}]}. The server never fetches the page and refuses HTML. Links already on the hub are skipped. Answers needs_confirm first when the links would be public.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["links"], "properties": { "links": { "type": "array", "maxItems": 50, "items": { "type": "object", "additionalProperties": false, "required": ["title", "url"], "properties": { "title": { "type": "string" }, "url": { "type": "string", "pattern": "^https://" } } } }, "source_url": { "type": "string", "pattern": "^https://", "description": "for the record only; never fetched" }, "confirm_public": { "type": "boolean" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "import-links", "path": "/api/hub?op=import-links", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    },
    {
      "name": "link_stats",
      "description": "Clicks per link per day (up to 90 days). Days with 1 to 9 clicks come back as {lt:10} (\"fewer than 10\"), and totals count only the days shown, so no single visitor can be picked out. No IP, cookie or device is kept.",
      "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "days": { "type": "integer", "minimum": 1, "maximum": 90 } } },
      "annotations": { "readOnlyHint": true },
      "x_nlo": { "method": "GET", "op": "stats", "path": "/api/hub?op=stats", "query": ["days"], "auth": "page_token", "ask_human_first": false, "status": "preview" }
    },
    {
      "name": "connect_account",
      "description": "Link another account. twitch / x / youtube: only by the human signing in on /claim (you get a link to give them). bluesky: step start (returns a code the human puts in their Bluesky profile or a post) then step verify (our server reads public.api.bsky.app only) → verified. bluesky step add, github, instagram, tiktok: typed in → shown as NOT verified. mastodon: step start returns what to check; YOU or the human's browser check the instance (our server never does), then step verify with checked:true → still NOT verified here. wallet: needs the human's browser (it signs), not this key.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["platform"], "properties": { "platform": { "enum": ["twitch", "x", "youtube", "wallet", "bluesky", "mastodon", "github", "instagram", "tiktok"] }, "step": { "enum": ["start", "verify", "add"] }, "handle": { "type": "string", "maxLength": 300 }, "checked": { "type": "boolean" }, "nonce": { "type": "string" }, "signature": { "type": "string" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "connect-account", "path": "/api/hub?op=connect-account", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    },
    {
      "name": "set_account_visibility",
      "description": "Show an account on the public page, or hide it. Every account starts hidden. Showing one answers needs_confirm first.",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["platform", "key", "visibility"], "properties": { "platform": { "type": "string" }, "key": { "type": "string", "description": "the account's key from get_hub" }, "visibility": { "enum": ["public", "hidden"] }, "confirm_public": { "type": "boolean" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "set-account-visibility", "path": "/api/hub?op=set-account-visibility", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    },
    {
      "name": "set_theme",
      "description": "Pick the page's look (light, dark, paper, forest, night, ocean) and, optionally, an accent colour #rrggbb. A colour that would make text hard to read is refused (accent_low_contrast, with the failing pairs).",
      "inputSchema": { "type": "object", "additionalProperties": false, "required": ["id"], "properties": { "id": { "enum": ["light", "dark", "paper", "forest", "night", "ocean"] }, "accent": { "type": "string", "pattern": "^(#[0-9a-fA-F]{6})?$" } } },
      "annotations": { "readOnlyHint": false },
      "x_nlo": { "method": "POST", "op": "set-theme", "path": "/api/hub?op=set-theme", "auth": "page_token", "ask_human_first": true, "status": "preview" }
    }
  ]
}
