{"title":"Tweeks Browser Extension API Reference","canonicalUrl":"https://www.tweeks.io/docs","markdownUrl":"https://www.tweeks.io/docs/tw-engine-extensions.md","jsonUrl":"https://www.tweeks.io/docs/tw-engine-extensions.json","lastModified":"2026-03-22","sourceFiles":["extension/shared/permission-system.js","extension/shared/script-compiler.js","extension/content/gm-api-constants.js","extension/content/content-runtime.js","extension/sw/gm-network-handlers.js","server/app/api/tw/inference/route.ts","server/app/api/tw/email/route.ts"],"permissionGroups":[{"id":"TW_inference","title":"TW_inference","description":"AI inference calls that leave the page, cross into the service worker, and hit the server-side inference endpoint.","members":["TW_inference"],"notes":["Recommended grant: @grant TW_inference","Also accepted: @grant TW.inference, @grant inference"]},{"id":"TW_email","title":"TW_email","description":"Email delivery and delayed email scheduling for the signed-in user.","members":["TW_email"],"notes":["Recommended grant: @grant TW_email","Also accepted: @grant TW.email, @grant email"]},{"id":"TW_avatar","title":"TW_avatar","description":"Avatar index, lookup, and self-profile operations backed by Tweeks avatar endpoints and local caches.","members":["TW_avatar"],"notes":["Recommended grant: @grant TW_avatar","Also accepted: @grant TW.avatar, @grant avatar"]},{"id":"TW_contextMenu","title":"TW_contextMenu","description":"Browser context-menu creation and cleanup in privileged extension context.","members":["TW_contextMenu","TW_removeContextMenu"],"notes":["@grant TW_contextMenu implicitly enables removal as well.","You can request only removal with @grant TW_removeContextMenu, but creation requires TW_contextMenu."]},{"id":"TW_notifications","title":"TW_notifications","description":"Local notification UI surfaced inside the Tweeks runtime: toasts and confirm-style alerts.","members":["TW_toast","TW_alert"],"notes":["@grant TW_notifications unlocks both TW_toast and TW_alert.","Individual grants like @grant TW_toast and @grant TW_alert are also accepted and are folded into the same permission group internally."]}],"extensions":[{"id":"TW_inference","anchor":"tw-inference","title":"TW_inference","category":"AI","summary":"Runs a prompt through Tweeks' configured inference model and returns plain text back to the userscript. The call is forwarded through the Tweeks runtime and requires the user to be signed in.","description":"Use this when your tweek needs a text response from Tweeks AI. The call is forwarded through the Tweeks runtime, requires the user to be signed in, and consumes the user's shared Tweeks quota when inference cost is configured above zero.","recommendedGrant":"TW_inference","acceptedGrants":["TW_inference","TW.inference","inference"],"permissionGroup":"TW_inference","execution":"background-plus-endpoint","callSurfaces":["TW_inference(prompt, data)","TW.inference(prompt, data)","GM.inference(prompt, data)"],"operations":[{"name":"inference","signature":"TW_inference(prompt, data?)","summary":"Submit a prompt and optional structured data, then receive a text completion.","auth":"required","returns":"Promise<string>","endpoints":["POST /api/tw/inference"],"notes":["prompt must be a non-empty string","prompt is capped at 10,000 characters","prompt + serialized data is capped at 50,000 characters","server max output is 4,096 tokens","client-side timeout is 120 seconds","successful calls consume shared Tweeks quota when FREE_QUOTA_INFERENCE_COST is above zero"]}],"notes":["This API is async-only.","The service worker rejects unauthenticated requests with a standardized sign-in error and the Tweeks panel shows a sign-in alert.","The server logs successful usage to inference_logs and counts those rows against quota."],"example":"// ==UserScript==\n// @grant TW_inference\n// ==/UserScript==\n\nconst summary = await TW.inference(\n  \"Summarize the visible article in three bullets.\",\n  { title: document.title, url: location.href }\n);\n\nconsole.log(summary);"},{"id":"TW_email","anchor":"tw-email","title":"TW_email","category":"Automation","summary":"Sends an email to the authenticated Tweeks account, immediately or on a delay. Delivery is handled by Tweeks after the runtime accepts the call.","description":"Use this to send a message to your own Tweeks email address right away or after a delay. Delivery is handled by Tweeks after the runtime accepts the call.","recommendedGrant":"TW_email","acceptedGrants":["TW_email","TW.email","email"],"permissionGroup":"TW_email","execution":"background-plus-endpoint","callSurfaces":["TW_email(subject, body, delay)","TW.email(subject, body, delay)","GM.email(subject, body, delay)"],"operations":[{"name":"email","signature":"TW_email(subject, body?, delay?)","summary":"Deliver a message to the current user's email address, optionally scheduled in the future.","auth":"required","returns":"Promise<{ success: true; messageId?: string; scheduled: boolean; runId?: string }>","endpoints":["POST /api/tw/email"],"notes":["subject must be a non-empty string and is capped at 200 characters","body must be a string if provided and is capped at 50,000 characters","delay must be a string like 10s, 5m, 1h, 24h, or 7d","maximum delay is 30 days","client-side timeout is 30 seconds"]}],"notes":["Newlines are stripped from the subject before delivery.","Unauthenticated calls return a sign-in error.","Delayed delivery returns a workflow run id instead of sending immediately."],"example":"// ==UserScript==\n// @grant TW_email\n// ==/UserScript==\n\nawait GM.email(\n  \"Daily page digest\",\n  document.body.innerText.slice(0, 4000),\n  \"15m\"\n);"},{"id":"TW_avatar","anchor":"tw-avatar","title":"TW_avatar","category":"Identity","summary":"Reads and updates public avatar profile data, with caching and action-specific sign-in rules.","description":"TW_avatar is the richest extension in the set. The single entry point multiplexes four actions for index lookup, batched profile fetches, and signed-in self profile reads and writes.","recommendedGrant":"TW_avatar","acceptedGrants":["TW_avatar","TW.avatar","avatar"],"permissionGroup":"TW_avatar","execution":"background-plus-cache","callSurfaces":["TW_avatar(\"getIndex\", payload)","TW.avatar(\"queryProfiles\", payload)","TW.avatar(\"upsertSelf\", payload)","GM.avatar(\"getSelf\", payload)"],"operations":[{"name":"getIndex","signature":"TW_avatar(\"getIndex\", { provider?, forceRefresh? })","summary":"Fetch the public index of user keys for a provider, optionally with a snapshot payload and local cache reuse.","auth":"optional","returns":"Promise<{ provider: string; revision: string; schemaVersion: number; mode: \"keys_only\" | \"snapshot\"; ttlSec: number; userKeys: string[]; profiles?: Record<string, PublicAvatarConfig>; fromCache: boolean; stale?: boolean; warning?: string }>","endpoints":["GET /api/tw/avatar/index"],"notes":["only the twitter provider is currently accepted","response mode may be keys_only or snapshot","If-None-Match is used to revalidate cached indexes","stale cached data may be returned when refresh fails"]},{"name":"queryProfiles","signature":"TW_avatar(\"queryProfiles\", { provider?, userKeys })","summary":"Resolve a batch of public avatar profiles by user key, mixing fresh cache entries with endpoint fetches.","auth":"optional","returns":"Promise<{ provider: string; ttlSec: number; ttlSecByUserKey: Record<string, number>; profiles: Record<string, PublicAvatarConfig> }>","endpoints":["POST /api/tw/avatar/query"],"notes":["payload.userKeys must be an array","invalid user keys fail the entire call","the service worker enforces a maximum batch size","missing profiles are cached with a shorter negative TTL"]},{"name":"upsertSelf","signature":"TW_avatar(\"upsertSelf\", { provider?, userKey, dna, animated, walking, talking, waving, bw, multiplayerEnabled })","summary":"Create or update the signed-in user's own avatar profile for a provider.","auth":"required","returns":"Promise<{ provider: string; profile: StoredAvatarProfile }>","endpoints":["PUT /api/tw/avatar/self"],"notes":["provider defaults to twitter and must currently stay there","dna must be 6-7 lowercase hex characters","animated, walking, talking, waving, bw, and multiplayerEnabled must all be booleans","401 responses are surfaced as AUTH_REQUIRED with authRequired: true; treat legacy UNAUTHORIZED as sign-in-required when supporting older extension builds"]},{"name":"getSelf","signature":"TW_avatar(\"getSelf\", { provider? })","summary":"Read the signed-in user's stored avatar profiles for a provider.","auth":"required","returns":"Promise<{ provider: string; profiles: Record<string, PublicAvatarConfig & { provider: string; userKey: string; multiplayerEnabled: boolean }>; count: number }>","endpoints":["GET /api/tw/avatar/self"],"notes":["private server-side fields are stripped before the result is posted back to page context","401 responses are surfaced as AUTH_REQUIRED with authRequired: true; treat legacy UNAUTHORIZED as sign-in-required when supporting older extension builds"]}],"notes":["This API uses session storage caches for both index and profile data.","The client-side timeout is 45 seconds.","Unsupported actions fail with Unsupported TW_avatar action."],"example":"// ==UserScript==\n// @grant TW_avatar\n// ==/UserScript==\n\nconst index = await TW.avatar(\"getIndex\", { provider: \"twitter\" });\nconst profiles = await TW.avatar(\"queryProfiles\", {\n  provider: \"twitter\",\n  userKeys: index.userKeys.slice(0, 5),\n});\n\nconsole.table(profiles.profiles);"},{"id":"TW_contextMenu","anchor":"tw-contextmenu","title":"TW_contextMenu","category":"Browser UI","summary":"Creates browser context-menu entries owned by the current script. Menus are persisted per script and cleaned up automatically when permissions or script state change.","description":"Context menus are managed in privileged extension context and persisted per script so they can be cleaned up reliably when permissions or script state change.","recommendedGrant":"TW_contextMenu","acceptedGrants":["TW_contextMenu","TW.contextMenu","contextMenu"],"permissionGroup":"TW_contextMenu","execution":"background","callSurfaces":["TW_contextMenu(title, callback, options)","TW.contextMenu(title, callback, options)","GM.contextMenu(title, callback, options)"],"operations":[{"name":"create","signature":"TW_contextMenu(title, callback, options?)","summary":"Create or update a context-menu item and bind its callback through the injected runtime.","auth":"none","returns":"string | null","endpoints":[],"notes":["title is required unless options.type === separator","callback must be a function","supported options include contexts, documentUrlPatterns, targetUrlPatterns, type, and parentId","if documentUrlPatterns are omitted, the script's @match patterns are used when available"]}],"notes":["Granting TW_contextMenu implicitly enables TW_removeContextMenu.","Menu ids are deterministic per script and title hash.","This stays inside the Tweeks browser runtime."],"example":"// ==UserScript==\n// @grant TW_contextMenu\n// ==/UserScript==\n\nconst menuId = TW.contextMenu(\"Summarize selection\", async () => {\n  const text = window.getSelection()?.toString() || \"\";\n  console.log(text);\n}, {\n  contexts: [\"selection\"],\n});"},{"id":"TW_removeContextMenu","anchor":"tw-removecontextmenu","title":"TW_removeContextMenu","category":"Browser UI","summary":"Removes a context-menu entry previously registered by the same script. Guarded by script ownership — the runtime refuses to remove menu ids the current script did not create.","description":"Removal is guarded by script ownership. The runtime will refuse to remove menu ids the current script did not create.","recommendedGrant":"TW_removeContextMenu","acceptedGrants":["TW_removeContextMenu","TW.removeContextMenu","removeContextMenu","TW_contextMenu","TW.contextMenu","contextMenu"],"permissionGroup":"TW_contextMenu","execution":"background","callSurfaces":["TW_removeContextMenu(menuId)","TW.removeContextMenu(menuId)","GM.removeContextMenu(menuId)"],"operations":[{"name":"remove","signature":"TW_removeContextMenu(menuId)","summary":"Remove a menu item owned by the current script and unregister its callback.","auth":"none","returns":"void","endpoints":[],"notes":["menuId must be a string","the menu must belong to the current script","missing menu items are tolerated during browser removal, but non-owned ids fail"]}],"notes":["TW_contextMenu implicitly grants this capability.","The service worker tracks menu ownership in session storage for cleanup."],"example":"// ==UserScript==\n// @grant TW_removeContextMenu\n// ==/UserScript==\n\nTW.removeContextMenu(\"ctx_abcd1234_demo\");"},{"id":"TW_toast","anchor":"tw-toast","title":"TW_toast","category":"Notifications","summary":"Shows a transient in-extension toast notification through the Tweeks UI layer. Stays inside the extension runtime and does not hit the network.","description":"TW_toast stays inside the extension runtime and does not hit the network. Permission checks still apply because the content runtime gates access through the shared notifications permission group.","recommendedGrant":"TW_notifications","acceptedGrants":["TW_notifications","TW.notifications","notifications","TW_toast","TW.toast","toast"],"permissionGroup":"TW_notifications","execution":"local-ui","callSurfaces":["TW_toast(message, type, title, duration)","TW.toast(message, type, title, duration)","GM.toast(message, type, title, duration)"],"operations":[{"name":"toast","signature":"TW_toast(message, type?, title?, duration?)","summary":"Display a toast in the Tweeks notification layer and receive its generated id.","auth":"none","returns":"Promise<{ success: true; toastId: string | null }>","endpoints":[],"notes":["message must be a non-empty string","type defaults to info","duration defaults to 4000 ms if omitted","client-side timeout is 5 seconds"]}],"notes":["Grant TW_notifications if you want both toast and alert.","This API initializes the local menu UI if needed."],"example":"// ==UserScript==\n// @grant TW_notifications\n// ==/UserScript==\n\nawait TW.toast(\"Saved filters for this page\", \"success\", \"Tweeks\", 2500);"},{"id":"TW_alert","anchor":"tw-alert","title":"TW_alert","category":"Notifications","summary":"Shows a confirm-style modal alert through the Tweeks notification layer. Resolves to the user's confirm or cancel choice.","description":"TW_alert is the interactive companion to TW_toast. It stays in the Tweeks UI and resolves to the user's confirm or cancel choice.","recommendedGrant":"TW_notifications","acceptedGrants":["TW_notifications","TW.notifications","notifications","TW_alert","TW.alert","alert"],"permissionGroup":"TW_notifications","execution":"local-ui","callSurfaces":["TW_alert(message, type, title, confirmText, cancelText)","TW.alert(message, type, title, confirmText, cancelText)","GM.alert(message, type, title, confirmText, cancelText)"],"operations":[{"name":"alert","signature":"TW_alert(message, type?, title?, confirmText?, cancelText?)","summary":"Display an interactive alert and resolve with the user's confirmation choice.","auth":"none","returns":"Promise<{ confirmed: boolean }>","endpoints":[],"notes":["message must be a non-empty string","type defaults to info","confirmText defaults to OK","client-side timeout is 60 seconds"]}],"notes":["Grant TW_notifications if you want both alert and toast.","The content runtime returns confirmed: true only when the user picks the confirm branch."],"example":"// ==UserScript==\n// @grant TW_notifications\n// ==/UserScript==\n\nconst { confirmed } = await GM.alert(\n  \"Archive this page state?\",\n  \"warning\",\n  \"Tweeks\",\n  \"Archive\",\n  \"Cancel\"\n);"}]}