{"_id":"@anydoor-dev/api","name":"@anydoor-dev/api","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@anydoor-dev/api","version":"0.1.0","description":"Typed API for authoring AnyDoor Script Plugins (apiVersion 1).","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/ZingerLittleBee/AnyDoor.git","directory":"tooling/packages/api"},"homepage":"https://github.com/ZingerLittleBee/AnyDoor/tree/main/tooling/packages/api#readme","keywords":["anydoor","plugin","macos"],"publishConfig":{"access":"public"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"types":"./dist/index.d.ts","main":"./dist/index.js","devDependencies":{"typescript":"^5.6.3"},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit && tsc -p tsconfig.type-tests.json"},"_id":"@anydoor-dev/api@0.1.0","bugs":{"url":"https://github.com/ZingerLittleBee/AnyDoor/issues"},"_integrity":"sha512-dkeROIp+lUb2j5yQ8sxCfPuUWDlFvm4qCjFXzO/D3lFreYizxU+vgF1vFWV6NcItLCN+fx/mdx7kItc0MUc+lQ==","_resolved":"/private/var/folders/bz/xfyl2rcn1tnd6fq_xslp7__h0000gn/T/e71e29fe0717125be453e560f16d295d/anydoor-dev-api-0.1.0.tgz","_from":"file:anydoor-dev-api-0.1.0.tgz","_nodeVersion":"24.11.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-dkeROIp+lUb2j5yQ8sxCfPuUWDlFvm4qCjFXzO/D3lFreYizxU+vgF1vFWV6NcItLCN+fx/mdx7kItc0MUc+lQ==","shasum":"01447c7ba95a312ad6915148f7697bb0c6f40037","tarball":"https://registry.npmjs.org/@anydoor-dev/api/-/api-0.1.0.tgz","fileCount":27,"unpackedSize":45354,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIF9d0+vSilwEWEmgkygaVD43SCM41k5zWY3tpeqQA0htAiAy+5Db3TQzcMfqKa/w+Z7A6x27TPX5BTBgjvO5T7caTg=="}]},"_npmUser":{"name":"zingerbee","email":"6970999@gmail.com"},"directories":{},"maintainers":[{"name":"zingerbee","email":"6970999@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/api_0.1.0_1784779272415_0.1626641733362344"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-23T04:01:12.108Z","0.1.0":"2026-07-23T04:01:12.540Z","modified":"2026-07-23T04:01:12.914Z"},"maintainers":[{"name":"zingerbee","email":"6970999@gmail.com"}],"description":"Typed API for authoring AnyDoor Script Plugins (apiVersion 1).","homepage":"https://github.com/ZingerLittleBee/AnyDoor/tree/main/tooling/packages/api#readme","keywords":["anydoor","plugin","macos"],"repository":{"type":"git","url":"git+https://github.com/ZingerLittleBee/AnyDoor.git","directory":"tooling/packages/api"},"bugs":{"url":"https://github.com/ZingerLittleBee/AnyDoor/issues"},"license":"MIT","readme":"# @anydoor-dev/api\n\nTyped authoring API for [AnyDoor](https://github.com/ZingerLittleBee/AnyDoor)\n**Script Plugins** — type definitions plus a thin runtime shim (`definePlugin`)\nand row descriptor builders (`actions`).\n\n[AnyDoor](https://github.com/ZingerLittleBee/AnyDoor) is a macOS menu-bar\ntoolbox with a Raycast-style command palette. A Script Plugin adds rows to that\npalette: searchable lists, paginated markdown detail pages, and row actions —\nauthored in TypeScript, bundled to a single plain-JS file with esbuild, and\nexecuted on the system JavaScriptCore inside a capability sandbox (a plugin can\nonly use what its manifest declares; no shell, no filesystem, no clipboard\nread).\n\nTargets the host `apiVersion: 1`. Milestone A makes no compatibility promise.\n\n## Getting started\n\nScaffold a ready-to-build plugin project (TypeScript + esbuild, this package\nalready wired in):\n\n```bash\npnpm dlx @anydoor-dev/create-plugin my-plugin\ncd my-plugin\npnpm install\npnpm build   # → dist/manifest.json + dist/bundle.js\n```\n\n`dist/` is a complete plugin package. Load it in AnyDoor either way:\n\n- **Install**: Settings → Plugins → Install Script Plugin… and pick `dist/`.\n- **Develop**: enable developer mode in Settings → Plugins, register the\n  directory as a Dev Plugin, and run `pnpm dev` — every rebuild hot-reloads in\n  the palette, with full error detail (message + stack) surfaced inline.\n\nWorked examples (a V2EX browser and a Hacker News browser) live in\n[`tooling/examples`](https://github.com/ZingerLittleBee/AnyDoor/tree/main/tooling/examples).\n\n## Install\n\nScaffolded projects depend on this package automatically\n(`@anydoor-dev/create-plugin`). To add it by hand:\n\n```bash\npnpm add @anydoor-dev/api\n```\n\n## Usage\n\n```ts\n// src/manifest.ts\nimport { defineManifest } from \"@anydoor-dev/api\";\n\nexport default defineManifest({\n  id: \"dev.anydoor.hn-top\",\n  name: \"Hacker News Top\",\n  description: \"Top stories from Hacker News.\",\n  version: \"1.0.0\",\n  apiVersion: 1,\n  capabilities: [\"fetch\", \"openURL\"],\n});\n```\n\n```ts\n// src/plugin.ts\nimport { definePlugin, actions } from \"@anydoor-dev/api\";\nimport manifest from \"./manifest.js\";\n\ndefinePlugin(manifest, {\n  async rows(query, api) {\n    const res = await api.fetch(\"https://hn.algolia.com/api/v1/search?tags=front_page\");\n    const hits = (JSON.parse(res.body).hits ?? []) as Array<{ objectID: string; title: string }>;\n    return hits.map((hit) => ({\n      id: hit.objectID,\n      title: hit.title,\n      action: actions.detail(),\n    }));\n  },\n  detail(rowId) {\n    return `# ${rowId}`;\n  },\n});\n```\n\n## Capability gating\n\n`definePlugin` narrows the `api` handed to each entry point to the capabilities\nthe manifest declares. The manifest is the single source of truth:\n\n```ts\ncapabilities: [\"fetch\"]         // in the manifest\n// ...\nasync rows(query, api) {\n  await api.fetch(\"...\");       // ok\n  await api.toast(\"info\", \"x\"); // compile error: `toast` not declared\n}\n```\n\n## Surface\n\n- `defineManifest(manifest)` — identity helper that captures the declared\n  capability tuple for type inference.\n- `definePlugin(manifest, handlers)` — registers `rows` / `list` / `detail` /\n  `action` with the host and injects the narrowed capability API.\n- `actions` — builders for row commit actions: `detail()`, `list(listId)`,\n  `openURL(url)`, `copy(text)`, `argument()`, `run(close?)`.\n- Types: `Manifest`, `Capability`, `Row`, `RowAction`, `DetailResult`,\n  `ListResult`, `FetchOptions`, `FetchResponse`, `Store`, `ToastKind`,\n  `DeclaredAPI`, `JSONValue`, and the per-capability function types.\n\n### Capabilities\n\n| Capability | Injected as | Signature |\n| --- | --- | --- |\n| `fetch` | `api.fetch` | `(url, options?) => Promise<FetchResponse>` |\n| `store` | `api.store` | `{ get, set, delete, keys }` |\n| `toast` | `api.toast` | `(kind, message) => Promise<void>` |\n| `pasteboard` | `api.copy` | `(text) => Promise<void>` |\n| `delay` | `api.delay` | `(ms) => Promise<void>` |\n| `openURL` | `api.openURL` | `(url) => Promise<void>` |\n| `translate` | `api.translate` | `(text) => Promise<string>` |\n\n`openURL` accepts only `http` and `https` URLs — the host rejects any other\nscheme (e.g. `file:` or a custom app scheme) with a rejected promise, so a\nplugin cannot use it to reach the filesystem or launch arbitrary apps. The same\nrestriction applies to a row's `openURL` commit action.\n\n`translate` translates text into the user's configured target language through\nthe translation service the user set up in AnyDoor's Settings. The plugin\ncannot choose the direction or the service; the source language is\nauto-detected. The promise rejects when no usable translation service is\nconfigured, when the provider fails, or when the text exceeds 10,000\ncharacters (free providers have hard length limits and LLM providers bill by\nvolume — which is also why this is a declared capability: it can spend the\nuser's paid API quota).\n\n### Entry points\n\n| Entry point | Signature | Purpose |\n| --- | --- | --- |\n| `rows` | `(query, api) => Row[] \\| Promise<Row[]>` | Root palette rows for a query. |\n| `list` | `(listId, query, api, cursor?) => ListResult \\| Promise<ListResult>` | Second-level rows for a committed `list` action, optionally paged (see below). |\n| `detail` | `(rowId, api, cursor?) => DetailResult \\| Promise<DetailResult>` | Markdown Detail for a row, optionally chunked (see below). |\n| `detailAction` | `(rowId, actionId, api) => DetailResult \\| Promise<DetailResult>` | Rebuild the Detail for a pressed footer action (see below). |\n| `action` | `(rowId, actionId, argument, api) => unknown` | Run a row action. |\n\n### Row fields\n\nBeyond `id` / `title` / `subtitle` / `symbol` / `action`, a row may declare:\n\n- `actionLabel` — footer label for the row's primary action (e.g. \"打开\").\n- `badge` — short status text rendered as a chip at the row's trailing edge\n  (e.g. `badge: on ? \"开启\" : \"关闭\"` on a toggle-style row).\n- `isChecked` — leading checkmark, for second-level option-style rows.\n\nAfter a row's `action` completes, the host re-invokes `rows()` and re-renders\nthe visible palette — so state the action just changed (a toggle's badge)\nupdates immediately; a `run(false)` (stay-open) toggle needs no extra wiring.\n\n## Detail markdown\n\nThe host renders a Detail with the system markdown parser (no third-party\nrenderer). Block structure it lays out:\n\n- headings (`#`…`######`), paragraphs, ordered/unordered lists, fenced code\n  blocks, thematic breaks (`---`)\n- **blockquotes** (`> …`) — drawn with a leading bar in secondary text, visually\n  distinct from body paragraphs. Use them for quoted or secondary content such\n  as comments; a multi-paragraph quote stays one visual unit, separate quotes\n  stay separate.\n- **image previews** — `![alt](url)` renders inline (async loaded, capped\n  height, tappable-link fallback on failure). Only `http`/`https` image URLs\n  load; any other scheme is dropped, never fetched. Bare image URLs in plain\n  text are *not* auto-detected — if your data source is plain text, rewrite\n  them into `![](url)` yourself (see `examples/v2ex`'s `withImagePreviews`).\n- inline bold / italic / inline code / links survive inside every block. Tapped\n  links are scheme-guarded like `openURL`.\n\n## Detail pagination (load more on scroll)\n\n`detail` may return a chunk instead of a plain string:\n\n```ts\nasync detail(rowId, api, cursor) {\n  const page = cursor === undefined ? 1 : Number(cursor);\n  const items = await fetchPage(rowId, page, api);\n  return {\n    markdown: page === 1 ? renderDocument(items) : renderItems(items),\n    more: items.length === PAGE_SIZE ? String(page + 1) : undefined,\n  };\n},\n```\n\n- A returned `more` cursor makes the host show a loading sentinel at the bottom\n  of the Detail; when the user scrolls to it, the host calls\n  `detail(rowId, api, cursor)` with that cursor and appends the returned\n  chunk's markdown below the rendered document.\n- The cursor is **opaque to the host** — encode whatever pagination state you\n  need (a page number, an API-issued token). It round-trips verbatim.\n- Omitting `more` (or returning a plain string) marks the document complete.\n- An empty chunk ends pagination cleanly; a thrown error keeps what is already\n  rendered and stops paginating. The host never issues more than one chunk\n  fetch at a time.\n\n## List pagination (load more on scroll)\n\n`list` may return a page instead of a plain row array:\n\n```ts\nasync list(listId, query, api, cursor) {\n  const start = cursor === undefined ? 0 : Number(cursor);\n  const rows = await fetchPage(listId, start, api);\n  return {\n    rows,\n    more: rows.length === PAGE_SIZE ? String(start + PAGE_SIZE) : undefined,\n  };\n},\n```\n\n- A returned `more` cursor makes the host show a loading sentinel below the\n  last row; when the user scrolls to it, the host calls\n  `list(listId, query, api, cursor)` with that cursor and appends the returned\n  page's rows. The cursor is opaque to the host, same as Detail pagination.\n- Omitting `more` (or returning a plain array) marks the list complete.\n- Appended rows whose id already exists are dropped, so overlapping pages (a\n  feed that shifted between fetches) cannot produce duplicate rows.\n- While the user is typing a second-level search, the sentinel hides —\n  filtering is local to the rows already loaded.\n- A failed page fetch keeps what is already shown and stops paginating; the\n  host never issues more than one page fetch at a time.\n\n## Detail actions (footer buttons)\n\nA `DetailResult` may declare footer `actions` — buttons the host renders in a\nbar at the bottom of the Detail:\n\n```ts\nasync detail(rowId, api) {\n  return {\n    markdown: renderOriginal(rowId),\n    actions: [{ id: \"translate\", label: \"翻译\" }],\n  };\n},\nasync detailAction(rowId, actionId, api) {\n  // Rebuild the whole document for the pressed action; the result replaces\n  // the rendered Detail (its own markdown, `more` cursor, and next actions).\n  return {\n    markdown: await renderTranslated(rowId, api),\n    actions: [{ id: \"original\", label: \"显示原文\" }],\n  };\n},\n```\n\n- Pressing a button drops the Detail to its loading state and calls\n  `detailAction(rowId, actionId, api)`; the result replaces the document\n  wholesale. Declare the *next* mode's actions on each result to build a\n  toggle (翻译 ⇄ 显示原文).\n- Actions are read from full documents only — an appended pagination chunk's\n  `actions` are ignored. Encode any mode into your `more` cursor so\n  scroll-loaded pages stay consistent with the rebuilt document.\n- Declaring `actions` without implementing `detailAction` surfaces an inline\n  error when pressed.\n","readmeFilename":"README.md","_rev":"1-0a1e84b1f1bf71cfa2e87d4db004ab06"}