@khalid-byldd/seo-tools-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # Byldd SEO Tools — Paperclip Plugin
2
+
3
+ A [Paperclip AI](https://paperclip.dev) plugin that connects autonomous SEO agents to the **Byldd SEO Backend**. It exposes keyword research, clustering, and content brief generation as agent tools — enabling a fully automated, Human-in-the-Loop SEO content pipeline.
4
+
5
+ ## What It Does
6
+
7
+ This plugin registers **12 agent tools** that proxy requests to the Byldd SEO backend API:
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+ | `runKeywordResearch` | Start an async keyword discovery + enrichment pipeline for a seed keyword |
12
+ | `getKeywordSessionStatus` | Poll a running keyword research job until completion |
13
+ | `listKeywordSessions` | List all past keyword research sessions |
14
+ | `clusterKeywords` | Group enriched keywords into topical hub-and-spoke clusters |
15
+ | `getClusterStatus` | Poll a running clustering job until completion |
16
+ | `listClusterSessions` | List all past clustering sessions |
17
+ | `generateContentBrief` | Generate a full content brief from a cluster or standalone keyword |
18
+ | `getBriefStatus` | Poll a running brief generation job until completion |
19
+ | `listContentBriefs` | List content briefs, optionally filtered by cluster |
20
+ | `rerunBriefStage` | Regenerate a single stage of the brief pipeline |
21
+ | `attachProofPoints` | Inject business-specific proof points (pricing, timelines, outcomes) into a brief |
22
+
23
+ ### UI Components
24
+
25
+ | Slot | Type | Description |
26
+ |------|------|-------------|
27
+ | `health-widget` | Dashboard Widget | Shows plugin health status |
28
+ | `keyword-research-tab` | Issue Detail Tab | Keyword research data tab on issues |
29
+
30
+ ### Data Handlers
31
+
32
+ | Key | Description |
33
+ |-----|-------------|
34
+ | `health` | Plugin health check |
35
+ | `sessions` | List keyword research sessions (backs the UI tab) |
36
+ | `enrichResult` | Full keyword metrics for a session (backs the UI tab) |
37
+
38
+ ## Prerequisites
39
+
40
+ - [Paperclip AI](https://paperclip.dev) instance running locally or deployed
41
+ - [Byldd SEO Backend](https://github.com/byldd/seo-byldd-backend) running and accessible
42
+ - Node.js ≥ 18
43
+
44
+ ## Setup
45
+
46
+ ### 1. Install Dependencies
47
+
48
+ ```bash
49
+ npm install
50
+ ```
51
+
52
+ ### 2. Build
53
+
54
+ ```bash
55
+ npm run build
56
+ ```
57
+
58
+ ### 3. Install Into Paperclip
59
+
60
+ Once published to npm, you can install the plugin directly using its package name:
61
+
62
+ ```bash
63
+ paperclipai plugin install @byldd/seo-tools
64
+ ```
65
+
66
+ > **Note:** For local development before publishing, you can still install it from the local path:
67
+ > ```bash
68
+ > paperclipai plugin install /path/to/seo-tools
69
+ > ```
70
+
71
+ ### 4. Configure Credentials
72
+
73
+ This plugin reads the backend URL and auth token from **Paperclip's plugin instance config** (not hardcoded in source). Set them after installing:
74
+
75
+ **Via CLI:**
76
+
77
+ ```bash
78
+ npx paperclipai plugin config:set byldd.seo-tools \
79
+ --payload-json '{"backendUrl":"https://seo-api.byldd.com","authToken":"<YOUR_JWT_TOKEN>"}'
80
+ ```
81
+
82
+ **Via Paperclip UI:**
83
+
84
+ 1. Open Paperclip → **Settings** → **Plugins** → **Seo Tools**
85
+ 2. Fill in:
86
+ - **Backend URL**: Your SEO backend base URL (e.g. `https://seo-api.byldd.com`)
87
+ - **Auth Token**: A valid JWT token for the backend
88
+
89
+ > **Note:** The auth token is a JWT with an expiry. Rotate it in the Paperclip UI when it expires — no code changes needed.
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ npm run dev # watch-mode rebuild (worker + manifest + UI)
95
+ npm run dev:ui # local UI dev server with hot-reload
96
+ npm test # run tests with vitest
97
+ npm run typecheck # type-check without emitting
98
+ ```
99
+
100
+ `npm run dev` rebuilds into `dist/` on file changes. Paperclip watches the output directory and hot-reloads the plugin worker automatically for local installs.
101
+
102
+ ## Project Structure
103
+
104
+ ```
105
+ seo-tools/
106
+ ├── src/
107
+ │ ├── worker.ts # Plugin worker — tool handlers + data handlers
108
+ │ ├── manifest.ts # Plugin manifest — tools, capabilities, config schema
109
+ │ ├── types/
110
+ │ │ └── index.ts # TypeScript interfaces for tool params + cluster shapes
111
+ │ └── ui/
112
+ │ └── index.tsx # React UI components (dashboard widget, detail tab)
113
+ ├── tests/
114
+ │ └── plugin.spec.ts # Plugin tests
115
+ ├── dist/ # Build output (gitignored)
116
+ ├── esbuild.config.mjs # esbuild bundler config
117
+ ├── rollup.config.mjs # Rollup bundler config (alternative)
118
+ ├── tsconfig.json
119
+ ├── vitest.config.ts
120
+ └── package.json
121
+ ```
122
+
123
+ ## Configuration Schema
124
+
125
+ The plugin declares an `instanceConfigSchema` in the manifest. Operators fill these fields in the Paperclip UI:
126
+
127
+ | Field | Type | Required | Description |
128
+ |-------|------|----------|-------------|
129
+ | `backendUrl` | `string` | ✅ | Base URL of the SEO backend API |
130
+ | `authToken` | `string` | ✅ | JWT auth token for the backend |
131
+
132
+ ## Capabilities
133
+
134
+ This plugin requests the following Paperclip capabilities:
135
+
136
+ | Capability | Purpose |
137
+ |------------|---------|
138
+ | `agent.tools.register` | Register SEO tools for agent use |
139
+ | `http.outbound` | Make HTTP requests to the SEO backend |
140
+ | `ui.detailTab.register` | Render the Keyword Research tab on issues |
141
+ | `ui.dashboardWidget.register` | Render the health dashboard widget |
142
+ | `events.subscribe` | Listen to Paperclip domain events |
143
+ | `plugin.state.read` | Read plugin-scoped state |
144
+ | `plugin.state.write` | Write plugin-scoped state |
145
+
146
+ ## Build Options
147
+
148
+ - `npm run build` — esbuild (default, faster)
149
+ - `npm run build:rollup` — rollup (alternative)
150
+
151
+ Both output to `dist/`.
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,220 @@
1
+ // src/manifest.ts
2
+ var manifest = {
3
+ id: "byldd.seo-tools",
4
+ apiVersion: 1,
5
+ version: "0.1.0",
6
+ displayName: "Seo Tools",
7
+ description: "A Paperclip plugin",
8
+ author: "Plugin Author",
9
+ categories: ["connector"],
10
+ capabilities: [
11
+ "agent.tools.register",
12
+ "http.outbound",
13
+ "ui.detailTab.register",
14
+ "events.subscribe",
15
+ "plugin.state.read",
16
+ "plugin.state.write",
17
+ "ui.dashboardWidget.register"
18
+ ],
19
+ instanceConfigSchema: {
20
+ type: "object",
21
+ properties: {
22
+ backendUrl: {
23
+ type: "string",
24
+ title: "Backend URL",
25
+ description: "Base URL of the SEO Byldd Server"
26
+ },
27
+ authToken: {
28
+ type: "string",
29
+ title: "Auth Token",
30
+ description: "JWT auth token for the SEO Byldd Server"
31
+ }
32
+ },
33
+ required: ["backendUrl", "authToken"]
34
+ },
35
+ entrypoints: {
36
+ worker: "./dist/worker.js",
37
+ ui: "./dist/ui"
38
+ },
39
+ ui: {
40
+ slots: [
41
+ {
42
+ type: "dashboardWidget",
43
+ id: "health-widget",
44
+ displayName: "Seo Tools Health",
45
+ exportName: "DashboardWidget"
46
+ },
47
+ {
48
+ type: "detailTab",
49
+ id: "keyword-research-tab",
50
+ displayName: "Keyword Research",
51
+ exportName: "KeywordResearchTab",
52
+ entityTypes: ["issue"]
53
+ }
54
+ ]
55
+ },
56
+ tools: [
57
+ {
58
+ name: "runKeywordResearch",
59
+ displayName: "Run Keyword Research",
60
+ description: "Call this when user asks for keyword research or Keyword Planner. Starts an async job \u2014 returns a keywordSessionRef immediately. Research takes ~20-30s. Wait ~20 seconds, then poll getKeywordSessionStatus every 10 seconds until status is 'completed' or 'failed' (max ~16 attempts, ~3 min).",
61
+ parametersSchema: {
62
+ type: "object",
63
+ properties: {
64
+ keyword: { type: "string" }
65
+ },
66
+ required: ["keyword"]
67
+ }
68
+ },
69
+ {
70
+ name: "getKeywordSessionStatus",
71
+ displayName: "Get Keyword Session Status",
72
+ description: "Poll the status of a KeywordSession started with runKeywordResearch. Wait ~20 seconds before the first poll, then call every 10 seconds until status is 'completed' or 'failed' (max ~16 attempts, ~3 min).",
73
+ parametersSchema: {
74
+ type: "object",
75
+ properties: {
76
+ keywordSessionRef: { type: "string" }
77
+ },
78
+ required: ["keywordSessionRef"]
79
+ }
80
+ },
81
+ {
82
+ name: "enrichKeywords",
83
+ displayName: "Enrich Keywords",
84
+ description: "Use only to re-enrich an EXISTING session that was previously discovered. For new keyword research, use runKeywordResearch which handles everything end-to-end.",
85
+ parametersSchema: {
86
+ type: "object",
87
+ properties: {
88
+ keywordSessionRef: { type: "string" }
89
+ },
90
+ required: ["keywordSessionRef"]
91
+ }
92
+ },
93
+ {
94
+ name: "getKeywordSuggestions",
95
+ displayName: "Get Keyword Suggestions",
96
+ description: "List raw expansion keywords for a session WITHOUT triggering the costly enrich step.",
97
+ parametersSchema: {
98
+ type: "object",
99
+ properties: {
100
+ keywordSessionRef: { type: "string" }
101
+ },
102
+ required: ["keywordSessionRef"]
103
+ }
104
+ },
105
+ {
106
+ name: "listKeywordSessions",
107
+ displayName: "List Keyword Sessions",
108
+ description: "List all past keyword research sessions.",
109
+ parametersSchema: {
110
+ type: "object",
111
+ properties: {}
112
+ }
113
+ },
114
+ {
115
+ name: "clusterKeywords",
116
+ displayName: "Cluster Keywords",
117
+ description: "Call this AFTER getKeywordSessionStatus returns 'completed' (and the user approves). Group enriched keywords into topical clusters. Returns a clusterId. Clustering can take ~20s to 1 min or more. Wait ~20 seconds, then poll getClusterStatus every 15 seconds until status is 'completed' (max ~18 attempts, ~5 min).",
118
+ parametersSchema: {
119
+ type: "object",
120
+ properties: {
121
+ keywordSessionRef: { type: "string" }
122
+ },
123
+ required: ["keywordSessionRef"]
124
+ }
125
+ },
126
+ {
127
+ name: "getClusterStatus",
128
+ displayName: "Get Cluster Status",
129
+ description: "Fetch status and results of a clustering session. After clusterKeywords, wait ~20 seconds, then poll every 15 seconds until status is completed (max ~18 attempts, ~5 min).",
130
+ parametersSchema: {
131
+ type: "object",
132
+ properties: {
133
+ clusterId: { type: "string" }
134
+ },
135
+ required: ["clusterId"]
136
+ }
137
+ },
138
+ {
139
+ name: "listClusterSessions",
140
+ displayName: "List Cluster Sessions",
141
+ description: "List all past clustering sessions.",
142
+ parametersSchema: {
143
+ type: "object",
144
+ properties: {}
145
+ }
146
+ },
147
+ {
148
+ name: "generateContentBrief",
149
+ displayName: "Generate Content Brief",
150
+ description: "Call this once user approves a cluster. Returns a briefId. You MUST then poll getBriefStatus until status is 'COMPLETED'. Start the async content-brief pipeline.",
151
+ parametersSchema: {
152
+ type: "object",
153
+ properties: {
154
+ source: { type: "string", enum: ["cluster", "standalone"] },
155
+ keyword: { type: "string" },
156
+ clusterSessionRef: { type: "string" },
157
+ isSelfReferentialListicle: { type: "boolean" }
158
+ },
159
+ required: ["source", "keyword"]
160
+ }
161
+ },
162
+ {
163
+ name: "getBriefStatus",
164
+ displayName: "Get Brief Status",
165
+ description: "Fetch the current state of a brief. Poll this until status is COMPLETED.",
166
+ parametersSchema: {
167
+ type: "object",
168
+ properties: {
169
+ briefId: { type: "string" }
170
+ },
171
+ required: ["briefId"]
172
+ }
173
+ },
174
+ {
175
+ name: "listContentBriefs",
176
+ displayName: "List Content Briefs",
177
+ description: "List content briefs, optionally filtered by cluster.",
178
+ parametersSchema: {
179
+ type: "object",
180
+ properties: {
181
+ clusterSessionRef: { type: "string" }
182
+ }
183
+ }
184
+ },
185
+ {
186
+ name: "rerunBriefStage",
187
+ displayName: "Rerun Brief Stage",
188
+ description: "Regenerate a single stage of the brief pipeline.",
189
+ parametersSchema: {
190
+ type: "object",
191
+ properties: {
192
+ briefId: { type: "string" },
193
+ stage: { type: "string" }
194
+ },
195
+ required: ["briefId", "stage"]
196
+ }
197
+ },
198
+ {
199
+ name: "attachProofPoints",
200
+ displayName: "Attach Proof Points",
201
+ description: "Inject business-specific proof points into a brief.",
202
+ parametersSchema: {
203
+ type: "object",
204
+ properties: {
205
+ briefId: { type: "string" },
206
+ pricingRanges: { type: "string" },
207
+ timelines: { type: "string" },
208
+ clientOutcomes: { type: "array", items: { type: "string" } },
209
+ methodology: { type: "string" }
210
+ },
211
+ required: ["briefId"]
212
+ }
213
+ }
214
+ ]
215
+ };
216
+ var manifest_default = manifest;
217
+ export {
218
+ manifest_default as default
219
+ };
220
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/manifest.ts"],
4
+ "sourcesContent": ["import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: \"byldd.seo-tools\",\n apiVersion: 1,\n version: \"0.1.0\",\n displayName: \"Seo Tools\",\n description: \"A Paperclip plugin\",\n author: \"Plugin Author\",\n categories: [\"connector\"],\n capabilities: [\n \"agent.tools.register\",\n \"http.outbound\",\n \"ui.detailTab.register\",\n \"events.subscribe\",\n \"plugin.state.read\",\n \"plugin.state.write\",\n \"ui.dashboardWidget.register\",\n ],\n instanceConfigSchema: {\n type: \"object\",\n properties: {\n backendUrl: {\n type: \"string\",\n title: \"Backend URL\",\n description: \"Base URL of the SEO Byldd Server\",\n },\n authToken: {\n type: \"string\",\n title: \"Auth Token\",\n description: \"JWT auth token for the SEO Byldd Server\",\n },\n },\n required: [\"backendUrl\", \"authToken\"],\n },\n entrypoints: {\n worker: \"./dist/worker.js\",\n ui: \"./dist/ui\",\n },\n ui: {\n slots: [\n {\n type: \"dashboardWidget\",\n id: \"health-widget\",\n displayName: \"Seo Tools Health\",\n exportName: \"DashboardWidget\",\n },\n {\n type: \"detailTab\",\n id: \"keyword-research-tab\",\n displayName: \"Keyword Research\",\n exportName: \"KeywordResearchTab\",\n entityTypes: [\"issue\"],\n },\n ],\n },\n tools: [\n {\n name: \"runKeywordResearch\",\n displayName: \"Run Keyword Research\",\n description:\n \"Call this when user asks for keyword research or Keyword Planner. Starts an async job \u2014 returns a keywordSessionRef immediately. Research takes ~20-30s. Wait ~20 seconds, then poll getKeywordSessionStatus every 10 seconds until status is 'completed' or 'failed' (max ~16 attempts, ~3 min).\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keyword: { type: \"string\" },\n },\n required: [\"keyword\"],\n },\n },\n {\n name: \"getKeywordSessionStatus\",\n displayName: \"Get Keyword Session Status\",\n description:\n \"Poll the status of a KeywordSession started with runKeywordResearch. Wait ~20 seconds before the first poll, then call every 10 seconds until status is 'completed' or 'failed' (max ~16 attempts, ~3 min).\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"enrichKeywords\",\n displayName: \"Enrich Keywords\",\n description:\n \"Use only to re-enrich an EXISTING session that was previously discovered. For new keyword research, use runKeywordResearch which handles everything end-to-end.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"getKeywordSuggestions\",\n displayName: \"Get Keyword Suggestions\",\n description:\n \"List raw expansion keywords for a session WITHOUT triggering the costly enrich step.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"listKeywordSessions\",\n displayName: \"List Keyword Sessions\",\n description: \"List all past keyword research sessions.\",\n parametersSchema: {\n type: \"object\",\n properties: {},\n },\n },\n {\n name: \"clusterKeywords\",\n displayName: \"Cluster Keywords\",\n description:\n \"Call this AFTER getKeywordSessionStatus returns 'completed' (and the user approves). Group enriched keywords into topical clusters. Returns a clusterId. Clustering can take ~20s to 1 min or more. Wait ~20 seconds, then poll getClusterStatus every 15 seconds until status is 'completed' (max ~18 attempts, ~5 min).\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"getClusterStatus\",\n displayName: \"Get Cluster Status\",\n description:\n \"Fetch status and results of a clustering session. After clusterKeywords, wait ~20 seconds, then poll every 15 seconds until status is completed (max ~18 attempts, ~5 min).\",\n parametersSchema: {\n type: \"object\",\n properties: {\n clusterId: { type: \"string\" },\n },\n required: [\"clusterId\"],\n },\n },\n {\n name: \"listClusterSessions\",\n displayName: \"List Cluster Sessions\",\n description: \"List all past clustering sessions.\",\n parametersSchema: {\n type: \"object\",\n properties: {},\n },\n },\n {\n name: \"generateContentBrief\",\n displayName: \"Generate Content Brief\",\n description:\n \"Call this once user approves a cluster. Returns a briefId. You MUST then poll getBriefStatus until status is 'COMPLETED'. Start the async content-brief pipeline.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n source: { type: \"string\", enum: [\"cluster\", \"standalone\"] },\n keyword: { type: \"string\" },\n clusterSessionRef: { type: \"string\" },\n isSelfReferentialListicle: { type: \"boolean\" },\n },\n required: [\"source\", \"keyword\"],\n },\n },\n {\n name: \"getBriefStatus\",\n displayName: \"Get Brief Status\",\n description:\n \"Fetch the current state of a brief. Poll this until status is COMPLETED.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n briefId: { type: \"string\" },\n },\n required: [\"briefId\"],\n },\n },\n {\n name: \"listContentBriefs\",\n displayName: \"List Content Briefs\",\n description: \"List content briefs, optionally filtered by cluster.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n clusterSessionRef: { type: \"string\" },\n },\n },\n },\n {\n name: \"rerunBriefStage\",\n displayName: \"Rerun Brief Stage\",\n description: \"Regenerate a single stage of the brief pipeline.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n briefId: { type: \"string\" },\n stage: { type: \"string\" },\n },\n required: [\"briefId\", \"stage\"],\n },\n },\n {\n name: \"attachProofPoints\",\n displayName: \"Attach Proof Points\",\n description: \"Inject business-specific proof points into a brief.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n briefId: { type: \"string\" },\n pricingRanges: { type: \"string\" },\n timelines: { type: \"string\" },\n clientOutcomes: { type: \"array\", items: { type: \"string\" } },\n methodology: { type: \"string\" },\n },\n required: [\"briefId\"],\n },\n },\n ],\n};\n\nexport default manifest;\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
1
+ // src/ui/index.tsx
2
+ import {
3
+ usePluginData
4
+ } from "@paperclipai/plugin-sdk/ui";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function KeywordResearchTab(_props) {
7
+ return null;
8
+ }
9
+ function DashboardWidget(_props) {
10
+ const { data, loading, error } = usePluginData("health");
11
+ if (loading) return /* @__PURE__ */ jsx("div", { children: "Loading plugin health..." });
12
+ if (error) return /* @__PURE__ */ jsxs("div", { children: [
13
+ "Plugin error: ",
14
+ error.message
15
+ ] });
16
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.5rem" }, children: [
17
+ /* @__PURE__ */ jsx("strong", { children: "Seo Tools" }),
18
+ /* @__PURE__ */ jsxs("div", { children: [
19
+ "Health: ",
20
+ data?.status ?? "unknown"
21
+ ] }),
22
+ /* @__PURE__ */ jsxs("div", { children: [
23
+ "Checked: ",
24
+ data?.checkedAt ?? "never"
25
+ ] })
26
+ ] });
27
+ }
28
+ export {
29
+ DashboardWidget,
30
+ KeywordResearchTab
31
+ };
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/index.tsx"],
4
+ "sourcesContent": ["import {\n usePluginData,\n type PluginWidgetProps,\n type PluginDetailTabProps,\n} from \"@paperclipai/plugin-sdk/ui\";\n\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype HealthData = {\n status: \"ok\" | \"degraded\" | \"error\";\n checkedAt: string;\n};\n\n// \u2500\u2500\u2500 Keyword Research Tab (UI rendering disabled) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function KeywordResearchTab(_props: PluginDetailTabProps) {\n return null;\n}\n\n// \u2500\u2500\u2500 Dashboard widget \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function DashboardWidget(_props: PluginWidgetProps) {\n const { data, loading, error } = usePluginData<HealthData>(\"health\");\n\n if (loading) return <div>Loading plugin health...</div>;\n if (error) return <div>Plugin error: {error.message}</div>;\n\n return (\n <div style={{ display: \"grid\", gap: \"0.5rem\" }}>\n <strong>Seo Tools</strong>\n <div>Health: {data?.status ?? \"unknown\"}</div>\n <div>Checked: {data?.checkedAt ?? \"never\"}</div>\n </div>\n );\n}\n"],
5
+ "mappings": ";AAAA;AAAA,EACE;AAAA,OAGK;AAoBe,cACF,YADE;AATf,SAAS,mBAAmB,QAA8B;AAC/D,SAAO;AACT;AAIO,SAAS,gBAAgB,QAA2B;AACzD,QAAM,EAAE,MAAM,SAAS,MAAM,IAAI,cAA0B,QAAQ;AAEnE,MAAI,QAAS,QAAO,oBAAC,SAAI,sCAAwB;AACjD,MAAI,MAAO,QAAO,qBAAC,SAAI;AAAA;AAAA,IAAe,MAAM;AAAA,KAAQ;AAEpD,SACE,qBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,SAAS,GAC3C;AAAA,wBAAC,YAAO,uBAAS;AAAA,IACjB,qBAAC,SAAI;AAAA;AAAA,MAAS,MAAM,UAAU;AAAA,OAAU;AAAA,IACxC,qBAAC,SAAI;AAAA;AAAA,MAAU,MAAM,aAAa;AAAA,OAAQ;AAAA,KAC5C;AAEJ;",
6
+ "names": []
7
+ }