@byldd/seo-tools-plugin 1.0.3 → 1.0.5
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 +19 -8
- package/dist/manifest.js +1 -0
- package/dist/manifest.js.map +2 -2
- package/dist/ui/index.js +193 -9
- package/dist/ui/index.js.map +4 -4
- package/dist/worker.js +102 -2
- package/dist/worker.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ A [Paperclip AI](https://paperclip.dev) plugin that connects autonomous SEO agen
|
|
|
4
4
|
|
|
5
5
|
## What It Does
|
|
6
6
|
|
|
7
|
-
This plugin registers **
|
|
7
|
+
This plugin registers **11 agent tools** that proxy requests to the Byldd SEO backend API:
|
|
8
8
|
|
|
9
9
|
| Tool | Description |
|
|
10
10
|
|------|-------------|
|
|
11
|
-
| `
|
|
11
|
+
| `startKeywordResearch` | Start an async keyword discovery + enrichment pipeline for a seed keyword |
|
|
12
12
|
| `getKeywordSessionStatus` | Poll a running keyword research job until completion |
|
|
13
|
-
| `
|
|
13
|
+
| `listKeywordResearchSessions` | List all past keyword research sessions |
|
|
14
14
|
| `clusterKeywords` | Group enriched keywords into topical hub-and-spoke clusters |
|
|
15
15
|
| `getClusterStatus` | Poll a running clustering job until completion |
|
|
16
16
|
| `listClusterSessions` | List all past clustering sessions |
|
|
@@ -25,7 +25,7 @@ This plugin registers **12 agent tools** that proxy requests to the Byldd SEO ba
|
|
|
25
25
|
| Slot | Type | Description |
|
|
26
26
|
|------|------|-------------|
|
|
27
27
|
| `health-widget` | Dashboard Widget | Shows plugin health status |
|
|
28
|
-
| `keyword-research-tab` | Issue Detail Tab |
|
|
28
|
+
| `keyword-research-tab` | Issue Detail Tab | Session picker + keyword metrics table (search volume, competition, intent, priority tier) with a per-keyword negative/exclude toggle |
|
|
29
29
|
|
|
30
30
|
### Data Handlers
|
|
31
31
|
|
|
@@ -35,6 +35,12 @@ This plugin registers **12 agent tools** that proxy requests to the Byldd SEO ba
|
|
|
35
35
|
| `sessions` | List keyword research sessions (backs the UI tab) |
|
|
36
36
|
| `enrichResult` | Full keyword metrics for a session (backs the UI tab) |
|
|
37
37
|
|
|
38
|
+
### UI Actions
|
|
39
|
+
|
|
40
|
+
| Key | Description |
|
|
41
|
+
|-----|-------------|
|
|
42
|
+
| `setKeywordNegative` | Set the negative/exclude flag on a single keyword from the Keyword Research tab |
|
|
43
|
+
|
|
38
44
|
## Prerequisites
|
|
39
45
|
|
|
40
46
|
- [Paperclip AI](https://paperclip.dev) instance running locally or deployed
|
|
@@ -76,14 +82,14 @@ This plugin reads the backend URL and auth token from **Paperclip's plugin insta
|
|
|
76
82
|
|
|
77
83
|
```bash
|
|
78
84
|
npx paperclipai plugin config:set byldd.seo-tools \
|
|
79
|
-
--payload-json '{"backendUrl":"
|
|
85
|
+
--payload-json '{"backendUrl":"<YOUR_BACKEND_URI>","authToken":"<YOUR_JWT_TOKEN>"}'
|
|
80
86
|
```
|
|
81
87
|
|
|
82
88
|
**Via Paperclip UI:**
|
|
83
89
|
|
|
84
90
|
1. Open Paperclip → **Settings** → **Plugins** → **Seo Tools**
|
|
85
91
|
2. Fill in:
|
|
86
|
-
- **Backend URL**: Your SEO backend base URL
|
|
92
|
+
- **Backend URL**: Your SEO backend base URL
|
|
87
93
|
- **Auth Token**: A valid JWT token for the backend
|
|
88
94
|
|
|
89
95
|
> **Note:** The auth token is a JWT with an expiry. Rotate it in the Paperclip UI when it expires — no code changes needed.
|
|
@@ -107,9 +113,13 @@ seo-tools/
|
|
|
107
113
|
│ ├── worker.ts # Plugin worker — tool handlers + data handlers
|
|
108
114
|
│ ├── manifest.ts # Plugin manifest — tools, capabilities, config schema
|
|
109
115
|
│ ├── types/
|
|
110
|
-
│ │ └── index.ts # TypeScript interfaces for tool params
|
|
116
|
+
│ │ └── index.ts # TypeScript interfaces/enums for tool params, cluster shapes, and UI rows
|
|
117
|
+
│ ├── utils/
|
|
118
|
+
│ │ ├── brief-view.ts # Projects backend brief payloads into the BriefWriterView shape
|
|
119
|
+
│ │ └── keyword-status.ts # Maps keyword enums (tier, competition, intent) to StatusBadge variants
|
|
111
120
|
│ └── ui/
|
|
112
|
-
│
|
|
121
|
+
│ ├── index.tsx # React UI components (dashboard widget, detail tab entry)
|
|
122
|
+
│ └── KeywordResearchTab.tsx # Keyword research detail tab (session picker + metrics table)
|
|
113
123
|
├── tests/
|
|
114
124
|
│ └── plugin.spec.ts # Plugin tests
|
|
115
125
|
├── dist/ # Build output (gitignored)
|
|
@@ -138,6 +148,7 @@ This plugin requests the following Paperclip capabilities:
|
|
|
138
148
|
| `agent.tools.register` | Register SEO tools for agent use |
|
|
139
149
|
| `http.outbound` | Make HTTP requests to the SEO backend |
|
|
140
150
|
| `ui.detailTab.register` | Render the Keyword Research tab on issues |
|
|
151
|
+
| `ui.action.register` | Register UI actions callable from plugin UI components (e.g. `setKeywordNegative`) |
|
|
141
152
|
| `ui.dashboardWidget.register` | Render the health dashboard widget |
|
|
142
153
|
| `events.subscribe` | Listen to Paperclip domain events |
|
|
143
154
|
| `plugin.state.read` | Read plugin-scoped state |
|
package/dist/manifest.js
CHANGED
package/dist/manifest.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/types/index.ts", "../src/manifest.ts"],
|
|
4
|
-
"sourcesContent": ["// Tool param types \u2014 mirror parametersSchema definitions in worker.ts\nexport interface RunKeywordResearchParams {\n keyword: string;\n}\n\nexport interface GetKeywordSessionStatusParams {\n keywordSessionRef: string;\n}\n\nexport interface ClusterKeywordsParams {\n keywordSessionRef: string;\n}\n\nexport interface GetClusterStatusParams {\n clusterId: string;\n}\n\nexport enum BriefSource {\n CLUSTER = \"cluster\",\n STANDALONE = \"standalone\",\n}\n\nexport interface GenerateContentBriefParams {\n source: BriefSource;\n keyword: string;\n clusterSessionRef?: string;\n isSelfReferentialListicle?: boolean;\n}\n\nexport interface GetBriefStatusParams {\n briefId: string;\n}\n\nexport interface ListContentBriefsParams {\n clusterSessionRef?: string;\n}\n\nexport interface RerunBriefStageParams {\n briefId: string;\n stage: RerunStage;\n}\n\nexport interface AttachProofPointsParams {\n briefId: string;\n pricingRanges?: string;\n timelines?: string;\n clientOutcomes?: string[];\n methodology?: string;\n}\n\n// Keyword session status\nexport enum KeywordSessionStatus {\n PENDING = \"pending\",\n DISCOVERING = \"discovering\",\n ENRICHING = \"enriching\",\n COMPLETED = \"completed\",\n FAILED = \"failed\",\n}\n\n// Raw KeywordSession item as returned by GET /api/keyword-research/.\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface KeywordSessionApiItem {\n _id: string;\n seedKeyword: string;\n status: KeywordSessionStatus;\n keywordCount?: number;\n createdAt: string;\n}\n\n// Output of listKeywordResearchSessions \u2014 one summary row per session.\nexport interface KeywordResearchSessionSummary {\n keywordSessionRef: string;\n seedKeyword: string;\n status: KeywordSessionStatus;\n keywordCount: number;\n createdAt: string;\n}\n\n// Brief pipeline rerun stages\nexport enum RerunStage {\n GEO = \"geo\",\n SCHEMA = \"schema\",\n OUTLINE = \"outline\",\n ENRICHMENT = \"enrichment\",\n INTENT = \"intent\",\n STRATEGY = \"strategy\",\n}\n\n// Clustering session status\nexport enum ClusterStatus {\n PENDING = \"pending\",\n FETCHING_SERPS = \"fetching_serps\",\n PROCESSING = \"processing\",\n ARCHITECTING = \"architecting\",\n COMPLETED = \"completed\",\n FAILED = \"failed\",\n}\n\n// Raw ClusterSession item as returned by GET /api/clustering/.\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface ClusterSessionApiItem {\n _id: string;\n seedKeyword?: string;\n status: ClusterStatus;\n keywordCount?: number;\n createdAt: string;\n}\n\n// Output of listClusterSessions \u2014 one summary row per session.\nexport interface ClusterSessionSummary {\n clusterId: string;\n seedKeyword?: string;\n status: ClusterStatus;\n keywordCount: number;\n createdAt: string;\n}\n\n// Raw ContentBrief item as returned by GET /api/content-brief/ (list).\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface ContentBriefApiItem {\n _id: string;\n keyword: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Output of listContentBriefs \u2014 one summary row per brief.\nexport interface ContentBriefSummary {\n briefId: string;\n keyword: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Cluster plan shapes\nexport interface ClusterPost {\n keyword: string;\n volume: number;\n intent: string;\n pageType: string;\n title: string;\n}\n\nexport interface ClusterGroup {\n name: string;\n posts: ClusterPost[];\n}\n\nexport interface ClusterPillar {\n keyword: string;\n volume: number;\n intent: string;\n pageType: string;\n title: string;\n}\n\nexport interface ClusterLandingPage {\n keyword: string;\n pageType: string;\n urlSlug: string;\n}\n\nexport interface ClusterInternalLinking {\n pillarLinksTo: string[];\n spokesLinkTo: string[];\n}\n\nexport interface ClusterArchitecture {\n pillar: ClusterPillar | null;\n clusters: ClusterGroup[];\n landingPage: ClusterLandingPage | null;\n internalLinking: ClusterInternalLinking | null;\n}\n\n// Brief writer-view shapes \u2014 the trimmed projection getBriefStatus returns to\n// the agent. Only the writer-facing fields survive; raw SERP/enrichment/schema\n// JSON, EEAT, and ops/meta are dropped here so RENDERING.md never has to.\nexport interface BriefOutlineSectionView {\n heading: string;\n level: number;\n wordCount: number;\n importance: string | null;\n format: string;\n guidance: string;\n subSections?: BriefOutlineSectionView[];\n}\n\nexport interface BriefInternalLinkView {\n anchor: string;\n targetUrl: string;\n reason: string;\n}\n\nexport interface BriefClusterSpokeView {\n keyword: string;\n title: string;\n url: string;\n}\n\nexport interface BriefWriterView {\n status: string;\n // Q1 \u2014 page identity\n pageType: string;\n intent: string;\n targetKeyword: string;\n targetAudience?: string;\n priorityScore?: number;\n priorityTier?: string;\n clusterPosition?: string;\n recommendedTitle?: string;\n metaDescription?: string;\n urlSlug?: string;\n // Q2 \u2014 coverage & depth\n supportingKeywords: string[];\n mustAnswer: string[];\n niceToAnswer: string[];\n targetWordCount?: number;\n competitorAvgWordCount?: number;\n outline: BriefOutlineSectionView[];\n // coverage checklist material (derived from coverageMatrix)\n requiredTopics: string[];\n contentGaps: string[];\n differentiatorOpportunities: string[];\n faq: string[];\n // Q3 \u2014 citability\n tldrBlock?: {\n text: string;\n wordCount: number;\n placement: string;\n fsTarget: boolean;\n };\n objectiveLanguageInstruction?: string;\n requiredSignals: string[];\n schemaRequired: string[];\n // Q4 \u2014 differentiation\n contentAngle?: string;\n competitorGap?: string;\n mustOwnTopics: string[];\n uniqueAssets: string[];\n proofPointsToInclude: string[];\n competitorBlindSpots: string[];\n competitorPagesToReference: string[];\n // Q5 \u2014 site connection\n internalLinks: BriefInternalLinkView[];\n internalLinksFrom: string[];\n isSelfReferentialListicle: boolean;\n clusterContext?: {\n pillarTitle: string;\n pillarUrl: string;\n spokes: BriefClusterSpokeView[];\n };\n // kept for the agent's polling / Manager report \u2014 not for the writer doc\n flags: { code: string; note?: string }[];\n}\n", "import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\nimport { RerunStage } from \"./types/index.js\";\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: \"startKeywordResearch\",\n displayName: \"Start a New Keyword Research\",\n description:\n \"ONLY entry point for new keyword research. Handles discovery AND enrichment in one pipeline. Returns a keywordSessionRef immediately \u2014 the job runs in the background. Research typically takes ~20-30s. Wait ~20 seconds, then poll getKeywordSessionStatus with that keywordSessionRef every 10 seconds until status is 'completed' or 'failed'. Max ~16 poll attempts (~3 minutes including the initial wait) before reporting timeout.\",\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 by startKeywordResearch. Wait ~20 seconds before the first poll (research takes ~20-30s), then call every 10 seconds. Stop when status is 'completed' or 'failed'. Max ~16 attempts (~3 min) \u2014 if still running after that, report timeout to the user and stop. When status is 'completed', the response contains the keywordSessionRef \u2014 the single id for this research run; pass it to clusterKeywords. Save it.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"listKeywordResearchSessions\",\n displayName: \"List Keyword Sessions\",\n description:\n \"List all past keyword research sessions with a brief overview \u2014 seedKeyword, status, and keyword count only.\",\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'. Pass the keywordSessionRef from that completed response (or from listKeywordResearchSessions). 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) before reporting timeout.\",\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 \"Poll the status of a clustering job. After clusterKeywords, wait ~20 seconds, then call every 15 seconds until status is 'completed' or 'failed'. Max ~18 attempts (~5 min) before reporting timeout. When completed, the response contains the full cluster plan \u2014 render it as a Markdown table immediately.\",\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:\n \"List all past clustering sessions with a brief overview \u2014 seedKeyword, status, and keyword count only.\",\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 or passed only a keyword. Returns a briefId. You MUST then poll getBriefStatus until status is 'COMPLETED'. When source is 'cluster', clusterSessionRef is REQUIRED (the clusterId from a completed clusterKeywords run); for source 'standalone' omit clusterSessionRef and pass only keyword.\",\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: {\n type: \"string\",\n enum: Object.values(RerunStage),\n },\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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",
|
|
4
|
+
"sourcesContent": ["// Tool param types \u2014 mirror parametersSchema definitions in worker.ts\nexport interface RunKeywordResearchParams {\n keyword: string;\n}\n\nexport interface GetKeywordSessionStatusParams {\n keywordSessionRef: string;\n}\n\nexport interface ClusterKeywordsParams {\n keywordSessionRef: string;\n}\n\nexport interface GetClusterStatusParams {\n clusterId: string;\n}\n\nexport enum BriefSource {\n CLUSTER = \"cluster\",\n STANDALONE = \"standalone\",\n}\n\nexport interface GenerateContentBriefParams {\n source: BriefSource;\n keyword: string;\n clusterSessionRef?: string;\n isSelfReferentialListicle?: boolean;\n}\n\nexport interface GetBriefStatusParams {\n briefId: string;\n}\n\nexport interface ListContentBriefsParams {\n clusterSessionRef?: string;\n}\n\nexport interface RerunBriefStageParams {\n briefId: string;\n stage: RerunStage;\n}\n\nexport interface AttachProofPointsParams {\n briefId: string;\n pricingRanges?: string;\n timelines?: string;\n clientOutcomes?: string[];\n methodology?: string;\n}\n\n// Keyword session status\nexport enum KeywordSessionStatus {\n PENDING = \"pending\",\n DISCOVERING = \"discovering\",\n ENRICHING = \"enriching\",\n COMPLETED = \"completed\",\n FAILED = \"failed\",\n}\n\n// Raw KeywordSession item as returned by GET /api/keyword-research/.\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface KeywordSessionApiItem {\n _id: string;\n seedKeyword: string;\n status: KeywordSessionStatus;\n keywordCount?: number;\n createdAt: string;\n}\n\n// Output of listKeywordResearchSessions \u2014 one summary row per session.\nexport interface KeywordResearchSessionSummary {\n keywordSessionRef: string;\n seedKeyword: string;\n status: KeywordSessionStatus;\n keywordCount: number;\n createdAt: string;\n}\n\n// Brief pipeline rerun stages\nexport enum RerunStage {\n GEO = \"geo\",\n SCHEMA = \"schema\",\n OUTLINE = \"outline\",\n ENRICHMENT = \"enrichment\",\n INTENT = \"intent\",\n STRATEGY = \"strategy\",\n}\n\n// Clustering session status\nexport enum ClusterStatus {\n PENDING = \"pending\",\n FETCHING_SERPS = \"fetching_serps\",\n PROCESSING = \"processing\",\n ARCHITECTING = \"architecting\",\n COMPLETED = \"completed\",\n FAILED = \"failed\",\n}\n\n// Raw ClusterSession item as returned by GET /api/clustering/.\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface ClusterSessionApiItem {\n _id: string;\n seedKeyword?: string;\n status: ClusterStatus;\n keywordCount?: number;\n createdAt: string;\n}\n\n// Output of listClusterSessions \u2014 one summary row per session.\nexport interface ClusterSessionSummary {\n clusterId: string;\n seedKeyword?: string;\n status: ClusterStatus;\n keywordCount: number;\n createdAt: string;\n}\n\n// Raw ContentBrief item as returned by GET /api/content-brief/ (list).\n// Mongo ObjectIds and Dates are serialized to strings over JSON.\nexport interface ContentBriefApiItem {\n _id: string;\n keyword: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Output of listContentBriefs \u2014 one summary row per brief.\nexport interface ContentBriefSummary {\n briefId: string;\n keyword: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// Cluster plan shapes\nexport interface ClusterPost {\n keyword: string;\n volume: number;\n intent: string;\n pageType: string;\n title: string;\n}\n\nexport interface ClusterGroup {\n name: string;\n posts: ClusterPost[];\n}\n\nexport interface ClusterPillar {\n keyword: string;\n volume: number;\n intent: string;\n pageType: string;\n title: string;\n}\n\nexport interface ClusterLandingPage {\n keyword: string;\n pageType: string;\n urlSlug: string;\n}\n\nexport interface ClusterInternalLinking {\n pillarLinksTo: string[];\n spokesLinkTo: string[];\n}\n\nexport interface ClusterArchitecture {\n pillar: ClusterPillar | null;\n clusters: ClusterGroup[];\n landingPage: ClusterLandingPage | null;\n internalLinking: ClusterInternalLinking | null;\n}\n\n// Brief writer-view shapes \u2014 the trimmed projection getBriefStatus returns to\n// the agent. Only the writer-facing fields survive; raw SERP/enrichment/schema\n// JSON, EEAT, and ops/meta are dropped here so RENDERING.md never has to.\nexport interface BriefOutlineSectionView {\n heading: string;\n level: number;\n wordCount: number;\n importance: string | null;\n format: string;\n guidance: string;\n subSections?: BriefOutlineSectionView[];\n}\n\nexport interface BriefInternalLinkView {\n anchor: string;\n targetUrl: string;\n reason: string;\n}\n\nexport interface BriefClusterSpokeView {\n keyword: string;\n title: string;\n url: string;\n}\n\nexport interface BriefWriterView {\n status: string;\n // Q1 \u2014 page identity\n pageType: string;\n intent: string;\n targetKeyword: string;\n targetAudience?: string;\n priorityScore?: number;\n priorityTier?: string;\n clusterPosition?: string;\n recommendedTitle?: string;\n metaDescription?: string;\n urlSlug?: string;\n // Q2 \u2014 coverage & depth\n supportingKeywords: string[];\n mustAnswer: string[];\n niceToAnswer: string[];\n targetWordCount?: number;\n competitorAvgWordCount?: number;\n outline: BriefOutlineSectionView[];\n // coverage checklist material (derived from coverageMatrix)\n requiredTopics: string[];\n contentGaps: string[];\n differentiatorOpportunities: string[];\n faq: string[];\n // Q3 \u2014 citability\n tldrBlock?: {\n text: string;\n wordCount: number;\n placement: string;\n fsTarget: boolean;\n };\n objectiveLanguageInstruction?: string;\n requiredSignals: string[];\n schemaRequired: string[];\n // Q4 \u2014 differentiation\n contentAngle?: string;\n competitorGap?: string;\n mustOwnTopics: string[];\n uniqueAssets: string[];\n proofPointsToInclude: string[];\n competitorBlindSpots: string[];\n competitorPagesToReference: string[];\n // Q5 \u2014 site connection\n internalLinks: BriefInternalLinkView[];\n internalLinksFrom: string[];\n isSelfReferentialListicle: boolean;\n clusterContext?: {\n pillarTitle: string;\n pillarUrl: string;\n spokes: BriefClusterSpokeView[];\n };\n // kept for the agent's polling / Manager report \u2014 not for the writer doc\n flags: { code: string; note?: string }[];\n}\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\nexport type SessionItem = { _id: string; seedKeyword: string; status: string };\n\nexport type HealthData = {\n status: \"ok\" | \"degraded\" | \"error\";\n checkedAt: string;\n};\n\nexport enum PriorityTier {\n HIGH = \"HIGH\",\n MEDIUM = \"MEDIUM\",\n LOW = \"LOW\",\n DEAD = \"DEAD\",\n}\n\nexport enum CompetitionValue {\n HIGH = \"HIGH\",\n MEDIUM = \"MEDIUM\",\n LOW = \"LOW\",\n}\n\nexport enum KeywordIntent {\n INFORMATIONAL = \"Informational\",\n COMMERCIAL = \"Commercial\",\n NAVIGATIONAL = \"Navigational\",\n TRANSACTIONAL = \"Transactional\",\n}\n\n// Mirrors @paperclipai/plugin-sdk/ui's StatusBadgeVariant string union.\nexport enum BadgeStatus {\n OK = \"ok\",\n INFO = \"info\",\n WARNING = \"warning\",\n ERROR = \"error\",\n PENDING = \"pending\",\n}\n\nexport type KeywordRow = {\n _id: string;\n keyword: string;\n avgMonthlySearches?: number;\n competitionValue?: CompetitionValue;\n intent?: KeywordIntent;\n priorityTier?: PriorityTier;\n priorityScore?: number;\n isNegative?: boolean;\n};\n\nexport type EnrichResult = {\n keywordSessionRef: string;\n seedKeyword: string | null;\n metrics: KeywordRow[];\n error?: string;\n};\n\nexport const NegativeOption = { YES: \"yes\", NO: \"no\" } as const;\n", "import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\nimport { RerunStage } from \"./types/index.js\";\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 \"ui.action.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: \"startKeywordResearch\",\n displayName: \"Start a New Keyword Research\",\n description:\n \"ONLY entry point for new keyword research. Handles discovery AND enrichment in one pipeline. Returns a keywordSessionRef immediately \u2014 the job runs in the background. Research typically takes ~20-30s. Wait ~20 seconds, then poll getKeywordSessionStatus with that keywordSessionRef every 10 seconds until status is 'completed' or 'failed'. Max ~16 poll attempts (~3 minutes including the initial wait) before reporting timeout.\",\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 by startKeywordResearch. Wait ~20 seconds before the first poll (research takes ~20-30s), then call every 10 seconds. Stop when status is 'completed' or 'failed'. Max ~16 attempts (~3 min) \u2014 if still running after that, report timeout to the user and stop. When status is 'completed', the response contains the keywordSessionRef \u2014 the single id for this research run; pass it to clusterKeywords. Save it.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n keywordSessionRef: { type: \"string\" },\n },\n required: [\"keywordSessionRef\"],\n },\n },\n {\n name: \"listKeywordResearchSessions\",\n displayName: \"List Keyword Sessions\",\n description:\n \"List all past keyword research sessions with a brief overview \u2014 seedKeyword, status, and keyword count only.\",\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'. Pass the keywordSessionRef from that completed response (or from listKeywordResearchSessions). 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) before reporting timeout.\",\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 \"Poll the status of a clustering job. After clusterKeywords, wait ~20 seconds, then call every 15 seconds until status is 'completed' or 'failed'. Max ~18 attempts (~5 min) before reporting timeout. When completed, the response contains the full cluster plan \u2014 render it as a Markdown table immediately.\",\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:\n \"List all past clustering sessions with a brief overview \u2014 seedKeyword, status, and keyword count only.\",\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 or passed only a keyword. Returns a briefId. You MUST then poll getBriefStatus until status is 'COMPLETED'. When source is 'cluster', clusterSessionRef is REQUIRED (the clusterId from a completed clusterKeywords run); for source 'standalone' omit clusterSessionRef and pass only keyword.\",\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: {\n type: \"string\",\n enum: Object.values(RerunStage),\n },\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
6
|
"names": ["RerunStage"]
|
|
7
7
|
}
|
package/dist/ui/index.js
CHANGED
|
@@ -1,25 +1,209 @@
|
|
|
1
1
|
// src/ui/index.tsx
|
|
2
2
|
import {
|
|
3
|
-
usePluginData
|
|
3
|
+
usePluginData as usePluginData2
|
|
4
4
|
} from "@paperclipai/plugin-sdk/ui";
|
|
5
|
+
|
|
6
|
+
// src/ui/KeywordResearchTab.tsx
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import {
|
|
9
|
+
usePluginData,
|
|
10
|
+
usePluginAction,
|
|
11
|
+
usePluginToast,
|
|
12
|
+
DataTable,
|
|
13
|
+
StatusBadge
|
|
14
|
+
} from "@paperclipai/plugin-sdk/ui";
|
|
15
|
+
|
|
16
|
+
// src/utils/keyword-status.ts
|
|
17
|
+
function tierStatus(tier) {
|
|
18
|
+
switch (tier) {
|
|
19
|
+
case "HIGH" /* HIGH */:
|
|
20
|
+
return "ok" /* OK */;
|
|
21
|
+
case "MEDIUM" /* MEDIUM */:
|
|
22
|
+
return "info" /* INFO */;
|
|
23
|
+
case "LOW" /* LOW */:
|
|
24
|
+
return "warning" /* WARNING */;
|
|
25
|
+
case "DEAD" /* DEAD */:
|
|
26
|
+
return "error" /* ERROR */;
|
|
27
|
+
default:
|
|
28
|
+
return "pending" /* PENDING */;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function competitionStatus(value) {
|
|
32
|
+
switch (value) {
|
|
33
|
+
case "LOW" /* LOW */:
|
|
34
|
+
return "ok" /* OK */;
|
|
35
|
+
case "MEDIUM" /* MEDIUM */:
|
|
36
|
+
return "warning" /* WARNING */;
|
|
37
|
+
case "HIGH" /* HIGH */:
|
|
38
|
+
return "error" /* ERROR */;
|
|
39
|
+
default:
|
|
40
|
+
return "pending" /* PENDING */;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function intentStatus(value) {
|
|
44
|
+
switch (value) {
|
|
45
|
+
case "Transactional" /* TRANSACTIONAL */:
|
|
46
|
+
return "ok" /* OK */;
|
|
47
|
+
case "Commercial" /* COMMERCIAL */:
|
|
48
|
+
return "warning" /* WARNING */;
|
|
49
|
+
case "Informational" /* INFORMATIONAL */:
|
|
50
|
+
return "info" /* INFO */;
|
|
51
|
+
case "Navigational" /* NAVIGATIONAL */:
|
|
52
|
+
return "pending" /* PENDING */;
|
|
53
|
+
default:
|
|
54
|
+
return "pending" /* PENDING */;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/ui/KeywordResearchTab.tsx
|
|
5
59
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
60
|
function KeywordResearchTab(_props) {
|
|
7
|
-
|
|
61
|
+
const { data: sessionsData, loading: sessionsLoading } = usePluginData("sessions");
|
|
62
|
+
const sessions = sessionsData?.sessions ?? [];
|
|
63
|
+
const [selectedRef, setSelectedRef] = useState(null);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!selectedRef && sessions.length > 0) setSelectedRef(sessions[0]._id);
|
|
66
|
+
}, [sessions, selectedRef]);
|
|
67
|
+
const { data, loading, error, refresh } = usePluginData(
|
|
68
|
+
"enrichResult",
|
|
69
|
+
{ keywordSessionRef: selectedRef ?? "" }
|
|
70
|
+
);
|
|
71
|
+
const rows = data?.metrics ?? [];
|
|
72
|
+
const setKeywordNegative = usePluginAction("setKeywordNegative");
|
|
73
|
+
const toast = usePluginToast();
|
|
74
|
+
const [pendingId, setPendingId] = useState(null);
|
|
75
|
+
async function onChangeNegative(row, isNegative) {
|
|
76
|
+
setPendingId(row._id);
|
|
77
|
+
try {
|
|
78
|
+
await setKeywordNegative({ keywordId: row._id, isNegative });
|
|
79
|
+
toast({
|
|
80
|
+
title: "Update successful",
|
|
81
|
+
body: "Keyword updated successfully",
|
|
82
|
+
tone: "success"
|
|
83
|
+
});
|
|
84
|
+
refresh();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
toast({
|
|
87
|
+
title: "Update failed",
|
|
88
|
+
body: e?.message ?? String(e),
|
|
89
|
+
tone: "error"
|
|
90
|
+
});
|
|
91
|
+
} finally {
|
|
92
|
+
setPendingId(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const numberedRows = rows.map((r, i) => ({
|
|
96
|
+
...r,
|
|
97
|
+
__rowNumber: i + 1
|
|
98
|
+
}));
|
|
99
|
+
const columns = [
|
|
100
|
+
{ key: "__rowNumber", header: "No.", width: "3rem" },
|
|
101
|
+
{ key: "keyword", header: "Keyword" },
|
|
102
|
+
{ key: "avgMonthlySearches", header: "Volume", sortable: true },
|
|
103
|
+
{
|
|
104
|
+
key: "competitionValue",
|
|
105
|
+
header: "Competition",
|
|
106
|
+
render: (v) => /* @__PURE__ */ jsx(
|
|
107
|
+
StatusBadge,
|
|
108
|
+
{
|
|
109
|
+
label: String(v ?? "\u2014"),
|
|
110
|
+
status: competitionStatus(v)
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "intent",
|
|
116
|
+
header: "Intent",
|
|
117
|
+
render: (v) => /* @__PURE__ */ jsx(
|
|
118
|
+
StatusBadge,
|
|
119
|
+
{
|
|
120
|
+
label: String(v ?? "\u2014"),
|
|
121
|
+
status: intentStatus(v)
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "priorityTier",
|
|
127
|
+
header: "Tier",
|
|
128
|
+
render: (v) => /* @__PURE__ */ jsx(
|
|
129
|
+
StatusBadge,
|
|
130
|
+
{
|
|
131
|
+
label: String(v ?? "\u2014"),
|
|
132
|
+
status: tierStatus(v)
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
},
|
|
136
|
+
{ key: "priorityScore", header: "Score", sortable: true },
|
|
137
|
+
{
|
|
138
|
+
key: "isNegative",
|
|
139
|
+
header: "Negative",
|
|
140
|
+
render: (_v, row) => /* @__PURE__ */ jsx(
|
|
141
|
+
"input",
|
|
142
|
+
{
|
|
143
|
+
type: "checkbox",
|
|
144
|
+
checked: row.isNegative ?? false,
|
|
145
|
+
disabled: pendingId === row._id,
|
|
146
|
+
onChange: (e) => onChangeNegative(row, e.target.checked)
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
if (sessionsLoading) return /* @__PURE__ */ jsx("div", { children: "Loading keyword discovery sessions\u2026" });
|
|
152
|
+
if (sessions.length === 0)
|
|
153
|
+
return /* @__PURE__ */ jsx("div", { children: "No keyword sessions yet. Run keyword research first." });
|
|
154
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.75rem", padding: "0.5rem" }, children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("label", { style: { display: "flex", gap: "0.5rem", alignItems: "center" }, children: [
|
|
156
|
+
/* @__PURE__ */ jsx("strong", { children: "Keyword Session:" }),
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
"select",
|
|
159
|
+
{
|
|
160
|
+
value: selectedRef ?? "",
|
|
161
|
+
onChange: (e) => setSelectedRef(e.target.value),
|
|
162
|
+
children: sessions.map((s) => /* @__PURE__ */ jsxs("option", { value: s._id, children: [
|
|
163
|
+
s.seedKeyword,
|
|
164
|
+
" (",
|
|
165
|
+
s.status,
|
|
166
|
+
")"
|
|
167
|
+
] }, s._id))
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
] }),
|
|
171
|
+
error && /* @__PURE__ */ jsxs("div", { children: [
|
|
172
|
+
"Error: ",
|
|
173
|
+
error.message
|
|
174
|
+
] }),
|
|
175
|
+
data?.error && /* @__PURE__ */ jsxs("div", { children: [
|
|
176
|
+
"Server: ",
|
|
177
|
+
data.error
|
|
178
|
+
] }),
|
|
179
|
+
/* @__PURE__ */ jsx(
|
|
180
|
+
DataTable,
|
|
181
|
+
{
|
|
182
|
+
columns,
|
|
183
|
+
rows: numberedRows,
|
|
184
|
+
loading,
|
|
185
|
+
emptyMessage: "No keywords for this session."
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] });
|
|
8
189
|
}
|
|
190
|
+
|
|
191
|
+
// src/ui/index.tsx
|
|
192
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
9
193
|
function DashboardWidget(_props) {
|
|
10
|
-
const { data, loading, error } =
|
|
11
|
-
if (loading) return /* @__PURE__ */
|
|
12
|
-
if (error) return /* @__PURE__ */
|
|
194
|
+
const { data, loading, error } = usePluginData2("health");
|
|
195
|
+
if (loading) return /* @__PURE__ */ jsx2("div", { children: "Loading plugin health..." });
|
|
196
|
+
if (error) return /* @__PURE__ */ jsxs2("div", { children: [
|
|
13
197
|
"Plugin error: ",
|
|
14
198
|
error.message
|
|
15
199
|
] });
|
|
16
|
-
return /* @__PURE__ */
|
|
17
|
-
/* @__PURE__ */
|
|
18
|
-
/* @__PURE__ */
|
|
200
|
+
return /* @__PURE__ */ jsxs2("div", { style: { display: "grid", gap: "0.5rem" }, children: [
|
|
201
|
+
/* @__PURE__ */ jsx2("strong", { children: "Seo Tools" }),
|
|
202
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
19
203
|
"Health: ",
|
|
20
204
|
data?.status ?? "unknown"
|
|
21
205
|
] }),
|
|
22
|
-
/* @__PURE__ */
|
|
206
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
23
207
|
"Checked: ",
|
|
24
208
|
data?.checkedAt ?? "never"
|
|
25
209
|
] })
|
package/dist/ui/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/ui/index.tsx"],
|
|
4
|
-
"sourcesContent": ["import {\n usePluginData,\n type PluginWidgetProps,\n
|
|
5
|
-
"mappings": ";AAAA;AAAA,EACE;AAAA,OAGK;
|
|
6
|
-
"names": []
|
|
3
|
+
"sources": ["../../src/ui/index.tsx", "../../src/ui/KeywordResearchTab.tsx", "../../src/utils/keyword-status.ts"],
|
|
4
|
+
"sourcesContent": ["import { HealthData } from \"@/types/index.js\";\nimport {\n usePluginData,\n type PluginWidgetProps,\n} from \"@paperclipai/plugin-sdk/ui\";\n\nexport { KeywordResearchTab } from \"./KeywordResearchTab.js\";\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\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", "import { useEffect, useState } from \"react\";\nimport {\n usePluginData,\n usePluginAction,\n usePluginToast,\n DataTable,\n StatusBadge,\n type DataTableColumn,\n type PluginDetailTabProps,\n} from \"@paperclipai/plugin-sdk/ui\";\nimport {\n EnrichResult,\n KeywordRow,\n SessionItem,\n CompetitionValue,\n KeywordIntent,\n PriorityTier,\n} from \"@/types/index.js\";\nimport {\n tierStatus,\n competitionStatus,\n intentStatus,\n} from \"@/utils/keyword-status.js\";\n\n// \u2500\u2500\u2500 Keyword Research Tab \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 NumberedKeywordRow = KeywordRow & { __rowNumber: number };\n\nexport function KeywordResearchTab(_props: PluginDetailTabProps) {\n // 1. Load the list of keyword sessions (worker `sessions` data handler).\n const { data: sessionsData, loading: sessionsLoading } = usePluginData<{\n sessions: SessionItem[];\n error?: string;\n }>(\"sessions\");\n const sessions = sessionsData?.sessions ?? [];\n\n const [selectedRef, setSelectedRef] = useState<string | null>(null);\n\n // Default to the first session once they load.\n useEffect(() => {\n if (!selectedRef && sessions.length > 0) setSelectedRef(sessions[0]._id);\n }, [sessions, selectedRef]);\n\n // 2. Load metrics for the chosen session (refetches when selectedRef changes).\n const { data, loading, error, refresh } = usePluginData<EnrichResult>(\n \"enrichResult\",\n { keywordSessionRef: selectedRef ?? \"\" },\n );\n\n const rows: KeywordRow[] = data?.metrics ?? [];\n\n const setKeywordNegative = usePluginAction(\"setKeywordNegative\");\n const toast = usePluginToast();\n const [pendingId, setPendingId] = useState<string | null>(null);\n\n async function onChangeNegative(row: KeywordRow, isNegative: boolean) {\n setPendingId(row._id);\n try {\n await setKeywordNegative({ keywordId: row._id, isNegative });\n toast({\n title: \"Update successful\",\n body: \"Keyword updated successfully\",\n tone: \"success\",\n });\n refresh();\n } catch (e) {\n toast({\n title: \"Update failed\",\n body: (e as { message?: string })?.message ?? String(e),\n tone: \"error\",\n });\n } finally {\n setPendingId(null);\n }\n }\n\n const numberedRows: NumberedKeywordRow[] = rows.map((r, i) => ({\n ...r,\n __rowNumber: i + 1,\n }));\n\n const columns: DataTableColumn<NumberedKeywordRow>[] = [\n { key: \"__rowNumber\", header: \"No.\", width: \"3rem\" },\n { key: \"keyword\", header: \"Keyword\" },\n { key: \"avgMonthlySearches\", header: \"Volume\", sortable: true },\n {\n key: \"competitionValue\",\n header: \"Competition\",\n render: (v) => (\n <StatusBadge\n label={String(v ?? \"\u2014\")}\n status={competitionStatus(v as CompetitionValue)}\n />\n ),\n },\n {\n key: \"intent\",\n header: \"Intent\",\n render: (v) => (\n <StatusBadge\n label={String(v ?? \"\u2014\")}\n status={intentStatus(v as KeywordIntent)}\n />\n ),\n },\n {\n key: \"priorityTier\",\n header: \"Tier\",\n render: (v) => (\n <StatusBadge\n label={String(v ?? \"\u2014\")}\n status={tierStatus(v as PriorityTier)}\n />\n ),\n },\n { key: \"priorityScore\", header: \"Score\", sortable: true },\n {\n key: \"isNegative\",\n header: \"Negative\",\n render: (_v, row) => (\n <input\n type=\"checkbox\"\n checked={row.isNegative ?? false}\n disabled={pendingId === row._id}\n onChange={(e) => onChangeNegative(row, e.target.checked)}\n />\n ),\n },\n ];\n\n if (sessionsLoading) return <div>Loading keyword discovery sessions\u2026</div>;\n if (sessions.length === 0)\n return <div>No keyword sessions yet. Run keyword research first.</div>;\n\n return (\n <div style={{ display: \"grid\", gap: \"0.75rem\", padding: \"0.5rem\" }}>\n <label style={{ display: \"flex\", gap: \"0.5rem\", alignItems: \"center\" }}>\n <strong>Keyword Session:</strong>\n <select\n value={selectedRef ?? \"\"}\n onChange={(e) => setSelectedRef(e.target.value)}\n >\n {sessions.map((s) => (\n <option key={s._id} value={s._id}>\n {s.seedKeyword} ({s.status})\n </option>\n ))}\n </select>\n </label>\n\n {error && <div>Error: {error.message}</div>}\n {data?.error && <div>Server: {data.error}</div>}\n\n <DataTable\n columns={columns as DataTableColumn<Record<string, unknown>>[]}\n rows={numberedRows as Record<string, unknown>[]}\n loading={loading}\n emptyMessage=\"No keywords for this session.\"\n />\n </div>\n );\n}\n", "import type { StatusBadgeVariant } from \"@paperclipai/plugin-sdk/ui\";\nimport {\n PriorityTier,\n CompetitionValue,\n KeywordIntent,\n BadgeStatus,\n} from \"@/types/index.js\";\n\nexport function tierStatus(tier?: PriorityTier): StatusBadgeVariant {\n switch (tier) {\n case PriorityTier.HIGH:\n return BadgeStatus.OK as StatusBadgeVariant;\n case PriorityTier.MEDIUM:\n return BadgeStatus.INFO as StatusBadgeVariant;\n case PriorityTier.LOW:\n return BadgeStatus.WARNING as StatusBadgeVariant;\n case PriorityTier.DEAD:\n return BadgeStatus.ERROR as StatusBadgeVariant;\n default:\n return BadgeStatus.PENDING as StatusBadgeVariant;\n }\n}\n\nexport function competitionStatus(value?: CompetitionValue): StatusBadgeVariant {\n switch (value) {\n case CompetitionValue.LOW:\n return BadgeStatus.OK as StatusBadgeVariant;\n case CompetitionValue.MEDIUM:\n return BadgeStatus.WARNING as StatusBadgeVariant;\n case CompetitionValue.HIGH:\n return BadgeStatus.ERROR as StatusBadgeVariant;\n default:\n return BadgeStatus.PENDING as StatusBadgeVariant;\n }\n}\n\nexport function intentStatus(value?: KeywordIntent): StatusBadgeVariant {\n switch (value) {\n case KeywordIntent.TRANSACTIONAL:\n return BadgeStatus.OK as StatusBadgeVariant;\n case KeywordIntent.COMMERCIAL:\n return BadgeStatus.WARNING as StatusBadgeVariant;\n case KeywordIntent.INFORMATIONAL:\n return BadgeStatus.INFO as StatusBadgeVariant;\n case KeywordIntent.NAVIGATIONAL:\n return BadgeStatus.PENDING as StatusBadgeVariant;\n default:\n return BadgeStatus.PENDING as StatusBadgeVariant;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["usePluginData", "jsx", "jsxs", "usePluginData"]
|
|
7
7
|
}
|
package/dist/worker.js
CHANGED
|
@@ -9994,6 +9994,89 @@ var RerunStage = /* @__PURE__ */ ((RerunStage2) => {
|
|
|
9994
9994
|
return RerunStage2;
|
|
9995
9995
|
})(RerunStage || {});
|
|
9996
9996
|
|
|
9997
|
+
// src/utils/brief-view.ts
|
|
9998
|
+
var SCHEMA_KEY_LABELS = {
|
|
9999
|
+
article: "Article",
|
|
10000
|
+
itemList: "ItemList",
|
|
10001
|
+
breadcrumbList: "BreadcrumbList",
|
|
10002
|
+
faqPage: "FAQPage",
|
|
10003
|
+
howTo: "HowTo",
|
|
10004
|
+
service: "Service",
|
|
10005
|
+
offer: "Offer"
|
|
10006
|
+
};
|
|
10007
|
+
function toBriefWriterView(d) {
|
|
10008
|
+
const brief = d.brief ?? {};
|
|
10009
|
+
const coverage = d.coverageMatrix ?? {};
|
|
10010
|
+
const intentSat = d.searchIntentSatisfaction ?? {};
|
|
10011
|
+
const diff = d.differentiationStrategy ?? {};
|
|
10012
|
+
const schemaBundle = d.schemaBundle ?? {};
|
|
10013
|
+
const mapOutline = (s) => ({
|
|
10014
|
+
heading: s.heading,
|
|
10015
|
+
level: s.level,
|
|
10016
|
+
wordCount: s.wordCount,
|
|
10017
|
+
importance: s.importance ?? null,
|
|
10018
|
+
format: s.formatNotes,
|
|
10019
|
+
guidance: s.keywordGuidance,
|
|
10020
|
+
...Array.isArray(s.subSections) && s.subSections.length ? { subSections: s.subSections.map(mapOutline) } : {}
|
|
10021
|
+
});
|
|
10022
|
+
return {
|
|
10023
|
+
status: d.status,
|
|
10024
|
+
pageType: d.template,
|
|
10025
|
+
intent: d.intent,
|
|
10026
|
+
targetKeyword: d.keyword,
|
|
10027
|
+
targetAudience: d.targetAudience,
|
|
10028
|
+
priorityScore: d.priorityScore,
|
|
10029
|
+
priorityTier: d.priorityTier,
|
|
10030
|
+
clusterPosition: d.clusterPosition,
|
|
10031
|
+
recommendedTitle: brief.title,
|
|
10032
|
+
metaDescription: brief.metaDescription,
|
|
10033
|
+
urlSlug: brief.urlSlug,
|
|
10034
|
+
supportingKeywords: d.supportingKeywords ?? [],
|
|
10035
|
+
mustAnswer: intentSat.mustAnswer ?? [],
|
|
10036
|
+
niceToAnswer: intentSat.niceToAnswer ?? [],
|
|
10037
|
+
targetWordCount: brief.targetWordCount,
|
|
10038
|
+
competitorAvgWordCount: brief.competitorAvgWordCount,
|
|
10039
|
+
outline: (brief.outline ?? []).map(mapOutline),
|
|
10040
|
+
requiredTopics: (coverage.requiredTopics ?? []).map((t) => t.topic),
|
|
10041
|
+
contentGaps: coverage.contentGaps ?? [],
|
|
10042
|
+
differentiatorOpportunities: coverage.differentiatorOpportunities ?? [],
|
|
10043
|
+
faq: (brief.faq ?? []).map((f) => f.q),
|
|
10044
|
+
tldrBlock: d.tldrBlock ? {
|
|
10045
|
+
text: d.tldrBlock.text,
|
|
10046
|
+
wordCount: d.tldrBlock.wordCount,
|
|
10047
|
+
placement: d.tldrBlock.placement,
|
|
10048
|
+
fsTarget: d.tldrBlock.fsTarget
|
|
10049
|
+
} : void 0,
|
|
10050
|
+
objectiveLanguageInstruction: d.objectiveLanguageInstruction,
|
|
10051
|
+
requiredSignals: d.geo?.requiredSignals ?? [],
|
|
10052
|
+
schemaRequired: Object.keys(SCHEMA_KEY_LABELS).filter((k) => schemaBundle[k]).map((k) => SCHEMA_KEY_LABELS[k]),
|
|
10053
|
+
contentAngle: d.contentAngle,
|
|
10054
|
+
competitorGap: d.competitorGap,
|
|
10055
|
+
mustOwnTopics: diff.mustOwnTopics ?? [],
|
|
10056
|
+
uniqueAssets: diff.uniqueAssets ?? [],
|
|
10057
|
+
proofPointsToInclude: diff.proofPointsToInclude ?? [],
|
|
10058
|
+
competitorBlindSpots: diff.competitorBlindSpots ?? [],
|
|
10059
|
+
competitorPagesToReference: d.competitorPagesToReference ?? [],
|
|
10060
|
+
internalLinks: (brief.internalLinks ?? []).map((l) => ({
|
|
10061
|
+
anchor: l.anchor,
|
|
10062
|
+
targetUrl: l.targetUrl,
|
|
10063
|
+
reason: l.reason
|
|
10064
|
+
})),
|
|
10065
|
+
internalLinksFrom: brief.internalLinksFrom ?? [],
|
|
10066
|
+
isSelfReferentialListicle: Boolean(d.isSelfReferentialListicle),
|
|
10067
|
+
clusterContext: d.clusterContext ? {
|
|
10068
|
+
pillarTitle: d.clusterContext.pillarTitle,
|
|
10069
|
+
pillarUrl: d.clusterContext.pillarUrl,
|
|
10070
|
+
spokes: (d.clusterContext.spokes ?? []).map((s) => ({
|
|
10071
|
+
keyword: s.keyword,
|
|
10072
|
+
title: s.title,
|
|
10073
|
+
url: s.url
|
|
10074
|
+
}))
|
|
10075
|
+
} : void 0,
|
|
10076
|
+
flags: (d.flags ?? []).map((f) => ({ code: f.code, note: f.note }))
|
|
10077
|
+
};
|
|
10078
|
+
}
|
|
10079
|
+
|
|
9997
10080
|
// src/worker.ts
|
|
9998
10081
|
var plugin = definePlugin({
|
|
9999
10082
|
async setup(ctx) {
|
|
@@ -10309,7 +10392,7 @@ var plugin = definePlugin({
|
|
|
10309
10392
|
const { briefId } = params;
|
|
10310
10393
|
try {
|
|
10311
10394
|
const json = await api(`/content-brief/${briefId}`, "GET");
|
|
10312
|
-
return { data: json?.data ?? {} };
|
|
10395
|
+
return { data: toBriefWriterView(json?.data ?? {}) };
|
|
10313
10396
|
} catch (e) {
|
|
10314
10397
|
return { error: String(e) };
|
|
10315
10398
|
}
|
|
@@ -10437,11 +10520,12 @@ var plugin = definePlugin({
|
|
|
10437
10520
|
`/keyword-research/${encodeURIComponent(keywordSessionRef)}/status`,
|
|
10438
10521
|
"GET"
|
|
10439
10522
|
);
|
|
10523
|
+
const keywords = json?.data?.keywords ?? [];
|
|
10440
10524
|
return {
|
|
10441
10525
|
keywordSessionRef,
|
|
10442
10526
|
seedKeyword: json?.data?.seedKeyword ?? null,
|
|
10443
10527
|
count: json?.data?.totalEnriched ?? 0,
|
|
10444
|
-
metrics:
|
|
10528
|
+
metrics: keywords
|
|
10445
10529
|
};
|
|
10446
10530
|
} catch (err) {
|
|
10447
10531
|
ctx.logger.error("enrichResult handler failed", {
|
|
@@ -10452,6 +10536,22 @@ var plugin = definePlugin({
|
|
|
10452
10536
|
}
|
|
10453
10537
|
}
|
|
10454
10538
|
);
|
|
10539
|
+
ctx.actions.register(
|
|
10540
|
+
"setKeywordNegative",
|
|
10541
|
+
async (params) => {
|
|
10542
|
+
const keywordId = params.keywordId;
|
|
10543
|
+
const isNegative = params.isNegative;
|
|
10544
|
+
if (!keywordId || typeof isNegative !== "boolean") {
|
|
10545
|
+
throw new Error("keywordId and isNegative are required");
|
|
10546
|
+
}
|
|
10547
|
+
const json = await api(
|
|
10548
|
+
`/keyword-research/keyword/${encodeURIComponent(keywordId)}/negative`,
|
|
10549
|
+
"PATCH",
|
|
10550
|
+
{ isNegative }
|
|
10551
|
+
);
|
|
10552
|
+
return json?.data ?? {};
|
|
10553
|
+
}
|
|
10554
|
+
);
|
|
10455
10555
|
},
|
|
10456
10556
|
async onHealth() {
|
|
10457
10557
|
return { status: "ok", message: "Plugin worker is running" };
|