{"_id":"@aimask/sdk","name":"@aimask/sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aimask/sdk","version":"0.1.0","description":"Zero-dependency TypeScript SDK and types for the aimask browser-extension LLM provider (window.aimask).","license":"MIT","author":{"name":"gantryops"},"homepage":"https://aimask.dev","repository":{"type":"git","url":"git+https://github.com/gantrydev/aimask.git","directory":"packages/sdk"},"bugs":{"url":"https://github.com/gantrydev/aimask/issues"},"type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"clean":"rm -rf dist","build:esm":"bun build ./src/index.ts --outfile ./dist/index.js --format esm --target browser","build:cjs":"bun build ./src/index.ts --outfile ./dist/index.cjs --format cjs --target node","build:types":"tsc -p tsconfig.json","build":"bun run clean && bun run build:esm && bun run build:cjs && bun run build:types","typecheck":"tsc --noEmit","test":"bun test","prepublishOnly":"bun run build"},"devDependencies":{"typescript":"^5.9.3","@types/bun":"^1.3.14"},"publishConfig":{"access":"public"},"keywords":["aimask","llm","byok","browser-extension","window.aimask"],"gitHead":"3cd8e9103b62c95dc1cb0f36f4b860e79b972077","_id":"@aimask/sdk@0.1.0","_nodeVersion":"24.17.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-Q1P55fjDxTcXuL8JrZxJoH//WcYiwkTtlJl76LhI6a9phL3qx828dGUtVWtlNa+r7sSMhu6A6xPCbf6bH0bUng==","shasum":"3165816aa43427f65c844a64bb0619b12a0e2d4a","tarball":"https://registry.npmjs.org/@aimask/sdk/-/sdk-0.1.0.tgz","fileCount":12,"unpackedSize":30130,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDFUVwmvXYv+p1f38r0czExzx49sZ0Q2MpdMdzn4grJfAiEA6Wjpx7sCVI2lNywhXmX8nuyEl7SHwTqUIp9994vpl44="}]},"_npmUser":{"name":"kastriotkastrati","email":"kastriotrkastrati@gmail.com"},"directories":{},"maintainers":[{"name":"kastriotkastrati","email":"kastriotrkastrati@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_0.1.0_1782515017862_0.9052006290516508"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-26T23:03:37.699Z","0.1.0":"2026-06-26T23:03:37.997Z","modified":"2026-06-26T23:03:38.223Z"},"maintainers":[{"name":"kastriotkastrati","email":"kastriotrkastrati@gmail.com"}],"description":"Zero-dependency TypeScript SDK and types for the aimask browser-extension LLM provider (window.aimask).","homepage":"https://aimask.dev","keywords":["aimask","llm","byok","browser-extension","window.aimask"],"repository":{"type":"git","url":"git+https://github.com/gantrydev/aimask.git","directory":"packages/sdk"},"author":{"name":"gantryops"},"bugs":{"url":"https://github.com/gantrydev/aimask/issues"},"license":"MIT","readme":"# @aimask/sdk\n\nZero-dependency TypeScript SDK and types for [aimask](https://aimask.dev) — bring your own LLM to any website, under per-origin spending budgets.\n\nThe aimask extension injects a frozen `window.aimask` provider into every page (the same pattern as `window.ethereum`). Your page asks for completions; the visitor's browser runs them against the visitor's own AI provider, under a budget the visitor granted your origin. You never hold an API key, run a backend, or get a bill. This package gives you the **types**, a **synchronous detector**, and **named error codes**.\n\n## Install\n\n```sh\nnpm install @aimask/sdk\n```\n\n## The contract: nothing throws\n\nEvery fallible call returns a **Result**, never a thrown error:\n\n```ts\ntype Result<T> =\n  | { ok: true; data: T; error: null }\n  | { ok: false; data: null; error: AimaskErrorPayload };\n\ntype AimaskErrorPayload = { code: number; name: ErrorName; message: string; data: unknown };\n```\n\nDetection is synchronous. Every request field is explicit — there are no defaults, because a default would silently spend the visitor's money. What you send is what runs.\n\n## Detect the provider\n\nFully synchronous — no polling, no timeouts. The extension injects `window.aimask` at `document_start`, so by the time your code runs the answer is already known.\n\n```ts\nimport { getAimask } from \"@aimask/sdk\";\n\nconst aimask = getAimask(); // Aimask | null\nif (aimask === null) {\n  // extension not installed — link the user to it\n}\n```\n\n## Request a session and chat\n\n```ts\nimport { getAimask } from \"@aimask/sdk\";\n\nconst aimask = getAimask();\nif (aimask === null) return;\n\nconst status = await aimask.availability();\n// \"available\" | \"requires-consent\" | \"unavailable\"\n\nconst opened = await aimask.requestSession({\n  needs: { context: null, vision: null, tools: null, json: null },\n  prefer: { tier: \"fast\", models: null },\n  intent: \"Summarize the current article\",\n});\nif (!opened.ok) return; // opened.error.code explains why\nconst session = opened.data;\n\nconst result = await session.chat({\n  messages: [\n    { role: \"user\", content: \"Hello\", name: null, tool_call_id: null, tool_calls: null },\n  ],\n  tools: null,\n  tool_choice: null,\n  response_format: null,\n  temperature: null,\n  max_tokens: 300,\n  stop: null,\n  signal: null,\n});\nif (!result.ok) return; // result.error.code\nconsole.log(result.data.message.content);\n```\n\n## Stream\n\n`chatStream` is synchronous and returns an async iterable of Result deltas. A setup failure, a mid-stream provider failure, and an abort all arrive as an `!ok` delta — the loop never throws.\n\n```ts\nconst controller = new AbortController();\n\nconst stream = session.chatStream({\n  messages: [{ role: \"user\", content: \"Hello\", name: null, tool_call_id: null, tool_calls: null }],\n  tools: null,\n  tool_choice: null,\n  response_format: null,\n  temperature: null,\n  max_tokens: 300,\n  stop: null,\n  signal: controller.signal,\n});\n\nfor await (const delta of stream) {\n  if (!delta.ok) break; // delta.error.code\n  if (delta.data.content) process.stdout.write(delta.data.content);\n}\n\n// controller.abort() ends the stream with a final USER_REJECTED (4001) delta.\n```\n\n## Handle errors\n\nErrors are values, not exceptions. Branch on `.ok`, then read `.error.code`:\n\n```ts\nimport { ERROR_CODES } from \"@aimask/sdk\";\n\nconst result = await session.chat(/* ... */);\nif (!result.ok) {\n  if (result.error.code === ERROR_CODES.USER_REJECTED) {\n    // the user declined the consent prompt\n  }\n  if (result.error.code === ERROR_CODES.BUDGET_EXCEEDED) {\n    // the per-origin allowance is exhausted\n  }\n  return;\n}\n```\n\n| Code | Name                     | Meaning                                        |\n| ---- | ------------------------ | ---------------------------------------------- |\n| 4001 | `USER_REJECTED`          | User rejected the request, or a stream aborted. |\n| 4100 | `UNAUTHORIZED`           | No active session for this origin.             |\n| 4200 | `BUDGET_EXCEEDED`        | Origin allowance exhausted.                    |\n| 4290 | `RATE_LIMITED`           | Origin over rate limit.                        |\n| 4400 | `CAPABILITY_UNAVAILABLE` | No model satisfies the requested capabilities. |\n| 4402 | `PROVIDER_UNFUNDED`      | OpenRouter account has no credit. Add funds.   |\n| 4900 | `DISCONNECTED`           | Extension locked or provider unreachable.      |\n| 5000 | `PROVIDER_ERROR`         | Upstream provider failure.                     |\n\n## Events\n\n```ts\naimask.on(\"disconnect\", () => {\n  // the session was revoked or the extension locked\n});\naimask.on(\"modelchanged\", (data) => {\n  // the resolved model changed\n});\naimask.on(\"budgetlow\", () => {\n  // 80% of the origin budget reached\n});\n```\n\n## Full API\n\nEverything exported, with exact signatures. `availability` is the one method that never returns a Result — a failed check collapses to `\"unavailable\"`.\n\n```ts\nfunction getAimask(): Aimask | null;\nfunction isAimaskAvailable(): boolean;\n\nconst ERROR_CODES: {\n  USER_REJECTED: 4001; UNAUTHORIZED: 4100; BUDGET_EXCEEDED: 4200; RATE_LIMITED: 4290;\n  CAPABILITY_UNAVAILABLE: 4400; PROVIDER_UNFUNDED: 4402; DISCONNECTED: 4900; PROVIDER_ERROR: 5000;\n};\n\ntype Aimask = {\n  isAimask: true;\n  protocolVersion: \"0.1\";\n  availability(): Promise<Availability>;\n  requestSession(req: SessionRequest | undefined): Promise<Result<AimaskSession>>;\n  on(event: AimaskEventType, handler: (data: unknown) => void): void;\n  off(event: AimaskEventType, handler: (data: unknown) => void): void;\n};\n\ntype AimaskSession = {\n  model: ResolvedModel;\n  chat(params: ClientChatParams): Promise<Result<ChatResult>>;\n  chatStream(params: ClientChatParams): AsyncIterable<Result<ChatDelta>>;\n  usage(): Promise<Result<UsageReport>>;\n  destroy(): void;\n};\n\ntype Availability = \"unavailable\" | \"requires-consent\" | \"available\";\ntype AimaskEventType = \"disconnect\" | \"modelchanged\" | \"budgetlow\";\n\ntype SessionRequest = {\n  needs: { context: number | null; vision: boolean | null; tools: boolean | null; json: boolean | null };\n  prefer: { tier: \"fast\" | \"balanced\" | \"best\" | null; models: Array<string> | null };\n  intent: string | null;\n};\n\ntype ResolvedModel = { id: string; context: number; vision: boolean; tools: boolean; json: boolean };\n\ntype ClientChatParams = ChatParams & { signal: AbortSignal | null };\n\ntype ChatParams = {\n  messages: Array<ChatMessage>;\n  tools: Array<Tool> | null;\n  tool_choice: ToolChoice | null;\n  response_format: ResponseFormat | null;\n  temperature: number | null;\n  max_tokens: number | null;\n  stop: Array<string> | null;\n};\n\ntype ChatMessage = {\n  role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n  content: string | Array<MessagePart>;\n  name: string | null;\n  tool_call_id: string | null;\n  tool_calls: Array<ToolCall> | null;\n};\n\ntype MessagePart =\n  | { type: \"text\"; text: string }\n  | { type: \"image_url\"; image_url: { url: string } };\n\ntype ToolCall = { id: string; type: \"function\"; function: { name: string; arguments: string } };\ntype Tool = { type: \"function\"; function: { name: string; description: string | null; parameters: object } };\ntype ToolChoice = \"auto\" | \"none\" | { type: \"function\"; function: { name: string } };\ntype ResponseFormat = { type: \"json_schema\"; json_schema: object };\n\ntype ChatResult = {\n  message: ChatMessage;\n  finish_reason: \"stop\" | \"length\" | \"tool_calls\" | \"content_filter\";\n  usage: ChatUsage;\n};\n\ntype ChatDelta = {\n  content: string | null;\n  tool_calls: Array<ToolCallDelta> | null;\n  finish_reason: \"stop\" | \"length\" | \"tool_calls\" | \"content_filter\" | null;\n  usage: ChatUsage | null;\n};\n\ntype ToolCallDelta = {\n  index: number;\n  id: string | null;\n  function: { name: string | null; arguments: string | null } | null;\n};\n\ntype ChatUsage = { prompt_tokens: number; completion_tokens: number; costUsd: number | null };\ntype UsageReport = { requests: number; spentUsd: number; grantedUsd: number; resetsAt: number };\n\ntype Result<T> =\n  | { ok: true; data: T; error: null }\n  | { ok: false; data: null; error: AimaskErrorPayload };\n\ntype AimaskErrorPayload = { code: number; name: ErrorName; message: string; data: unknown };\ntype ErrorName =\n  | \"USER_REJECTED\" | \"UNAUTHORIZED\" | \"BUDGET_EXCEEDED\" | \"RATE_LIMITED\"\n  | \"CAPABILITY_UNAVAILABLE\" | \"PROVIDER_UNFUNDED\" | \"DISCONNECTED\" | \"PROVIDER_ERROR\";\n```\n\n## Notes\n\n- **`window.aimask` is the protocol; this SDK is the client.** Any extension implementing the protocol works; you code against the protocol, not against aimask.\n- **Sessions are cheap handles, not server state.** History lives in your page — `messages` is always the full conversation.\n- **Spend is the permission.** The consent prompt is a budget grant; a site can never spend more than the visitor knowingly granted it.\n- **Outputs are untrusted.** The visitor controls the model, so treat completions like user input — never as authenticated data.\n\nSpec: https://github.com/gantrydev/aimask/blob/main/SPEC.md · Site: https://aimask.dev\n","readmeFilename":"README.md","_rev":"1-382d6d33d7260daa26691f12321d0f3d"}