@byldd/seo-tools-plugin 1.0.4 → 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 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 **12 agent tools** that proxy requests to the Byldd SEO backend API:
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
- | `runKeywordResearch` | Start an async keyword discovery + enrichment pipeline for a seed keyword |
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
- | `listKeywordSessions` | List all past keyword research sessions |
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 | Keyword research data tab on issues |
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":"https://seo-api.byldd.com","authToken":"<YOUR_JWT_TOKEN>"}'
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 (e.g. `https://seo-api.byldd.com`)
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 + cluster shapes
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
- └── index.tsx # React UI components (dashboard widget, detail tab)
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
@@ -22,6 +22,7 @@ var manifest = {
22
22
  "agent.tools.register",
23
23
  "http.outbound",
24
24
  "ui.detailTab.register",
25
+ "ui.action.register",
25
26
  "events.subscribe",
26
27
  "plugin.state.read",
27
28
  "plugin.state.write",
@@ -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
- return null;
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 } = usePluginData("health");
11
- if (loading) return /* @__PURE__ */ jsx("div", { children: "Loading plugin health..." });
12
- if (error) return /* @__PURE__ */ jsxs("div", { children: [
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__ */ jsxs("div", { style: { display: "grid", gap: "0.5rem" }, children: [
17
- /* @__PURE__ */ jsx("strong", { children: "Seo Tools" }),
18
- /* @__PURE__ */ jsxs("div", { children: [
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__ */ jsxs("div", { children: [
206
+ /* @__PURE__ */ jsxs2("div", { children: [
23
207
  "Checked: ",
24
208
  data?.checkedAt ?? "never"
25
209
  ] })
@@ -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 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": []
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: json?.data?.keywords ?? []
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" };