myaidev-method 0.3.4 → 0.3.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/.claude-plugin/plugin.json +0 -1
- package/.env.example +5 -4
- package/CHANGELOG.md +2 -2
- package/CONTENT_CREATION_GUIDE.md +489 -3211
- package/DEVELOPER_USE_CASES.md +1 -1
- package/MODULAR_INSTALLATION.md +2 -2
- package/README.md +39 -33
- package/TECHNICAL_ARCHITECTURE.md +1 -1
- package/USER_GUIDE.md +242 -190
- package/agents/content-editor-agent.md +90 -0
- package/agents/content-planner-agent.md +97 -0
- package/agents/content-research-agent.md +62 -0
- package/agents/content-seo-agent.md +101 -0
- package/agents/content-writer-agent.md +69 -0
- package/agents/infographic-analyzer-agent.md +63 -0
- package/agents/infographic-designer-agent.md +72 -0
- package/bin/cli.js +776 -422
- package/{content-rules.example.md → content-rules-example.md} +2 -2
- package/dist/mcp/health-check.js +82 -68
- package/dist/mcp/mcp-config.json +8 -0
- package/dist/mcp/openstack-server.js +1746 -1262
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +21 -4
- package/package.json +181 -184
- package/skills/company-config/SKILL.md +133 -0
- package/skills/configure/SKILL.md +1 -1
- package/skills/myai-configurator/SKILL.md +77 -0
- package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
- package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
- package/skills/myai-content-enrichment/SKILL.md +114 -0
- package/skills/myai-content-ideation/SKILL.md +288 -0
- package/skills/myai-content-ideation/evals/evals.json +182 -0
- package/skills/myai-content-production-coordinator/SKILL.md +946 -0
- package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
- package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
- package/skills/myai-content-writer/SKILL.md +333 -0
- package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
- package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
- package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
- package/skills/myai-skill-builder/SKILL.md +699 -0
- package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
- package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
- package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
- package/skills/myai-skill-builder/assets/eval_review.html +131 -0
- package/skills/myai-skill-builder/references/schemas.md +211 -0
- package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
- package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
- package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
- package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
- package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
- package/skills/myai-skill-builder/scripts/utils.py +123 -0
- package/skills/myai-visual-generator/SKILL.md +125 -0
- package/skills/myai-visual-generator/evals/evals.json +155 -0
- package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
- package/skills/myai-visual-generator/references/research-visuals.md +57 -0
- package/skills/myai-visual-generator/references/services.md +89 -0
- package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
- package/skills/myaidev-figma/SKILL.md +212 -0
- package/skills/myaidev-figma/capture.js +133 -0
- package/skills/myaidev-figma/crawl.js +130 -0
- package/skills/myaidev-figma-configure/SKILL.md +130 -0
- package/skills/openstack-manager/SKILL.md +1 -1
- package/skills/payloadcms-publisher/SKILL.md +141 -77
- package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
- package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
- package/skills/security-auditor/SKILL.md +1 -1
- package/src/cli/commands/addon.js +105 -7
- package/src/config/workflows.js +172 -228
- package/src/lib/ascii-banner.js +197 -182
- package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
- package/src/lib/installation-detector.js +93 -59
- package/src/lib/payloadcms-utils.js +285 -510
- package/src/lib/workflow-installer.js +55 -0
- package/src/mcp/health-check.js +82 -68
- package/src/mcp/openstack-server.js +1746 -1262
- package/src/scripts/configure-visual-apis.js +224 -173
- package/src/scripts/configure-wordpress-mcp.js +96 -66
- package/src/scripts/init/install.js +109 -85
- package/src/scripts/init-project.js +138 -67
- package/src/scripts/utils/write-content.js +67 -52
- package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
- package/src/templates/claude/CLAUDE.md +19 -12
- package/hooks/hooks.json +0 -26
- package/skills/content-coordinator/SKILL.md +0 -130
- package/skills/content-enrichment/SKILL.md +0 -80
- package/skills/content-writer/SKILL.md +0 -285
- package/skills/skill-builder/SKILL.md +0 -417
- package/skills/visual-generator/SKILL.md +0 -140
- /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/writer-agent.md +0 -0
|
@@ -1,96 +1,160 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: payloadcms-publisher
|
|
3
|
-
description: Publishes markdown content to PayloadCMS with Lexical rich text conversion and media
|
|
3
|
+
description: Publishes markdown content to PayloadCMS with automatic Lexical rich text conversion, SEO metadata generation, and media uploads. Use when publishing content to a PayloadCMS instance, uploading articles to Payload CMS, or when the user mentions PayloadCMS publishing.
|
|
4
4
|
argument-hint: "[file.md] [--status draft|published] [--collection posts] [--dry-run]"
|
|
5
|
-
allowed-tools: [Read,
|
|
5
|
+
allowed-tools: [Read, Edit, Bash, Glob, Grep]
|
|
6
6
|
disable-model-invocation: true
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# PayloadCMS Publisher
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
## Workflow
|
|
23
|
-
|
|
24
|
-
1. **Read source file** — Parse frontmatter and markdown content
|
|
25
|
-
2. **Load credentials** — Read from .env: `PAYLOADCMS_URL`, `PAYLOADCMS_EMAIL`, `PAYLOADCMS_PASSWORD`
|
|
26
|
-
3. **Authenticate** — Login via `POST /api/users/login`
|
|
27
|
-
4. **Convert markdown to Lexical:**
|
|
28
|
-
- Headings → heading nodes with proper levels
|
|
29
|
-
- Paragraphs → paragraph nodes with text children
|
|
30
|
-
- Lists → list nodes (ordered/unordered)
|
|
31
|
-
- Code blocks → code nodes with language
|
|
32
|
-
- Links → link nodes with URL
|
|
33
|
-
- Images → upload nodes (upload media first)
|
|
34
|
-
- Bold/italic → text nodes with format flags
|
|
35
|
-
5. **Handle media:**
|
|
36
|
-
- Extract image references from content
|
|
37
|
-
- Upload images via `POST /api/media`
|
|
38
|
-
- Replace references with upload IDs
|
|
39
|
-
6. **Publish:**
|
|
40
|
-
- New: `POST /api/{collection}`
|
|
41
|
-
- Update: `PATCH /api/{collection}/{id}`
|
|
42
|
-
- Set `_status` field based on --status flag
|
|
43
|
-
7. **Report:** Document URL and ID
|
|
44
|
-
|
|
45
|
-
## Lexical Format
|
|
46
|
-
|
|
47
|
-
PayloadCMS uses Lexical editor format:
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"root": {
|
|
51
|
-
"type": "root",
|
|
52
|
-
"children": [
|
|
53
|
-
{
|
|
54
|
-
"type": "heading",
|
|
55
|
-
"tag": "h2",
|
|
56
|
-
"children": [{"type": "text", "text": "Title"}]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"type": "paragraph",
|
|
60
|
-
"children": [{"type": "text", "text": "Content"}]
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
11
|
+
Publish markdown files to PayloadCMS via the bundled script. The script handles authentication, markdown-to-Lexical conversion, image uploads, and API calls.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Credentials in `.env`:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
PAYLOADCMS_URL=https://your-payload-instance.com
|
|
19
|
+
PAYLOADCMS_EMAIL=admin@example.com
|
|
20
|
+
PAYLOADCMS_PASSWORD=your-password
|
|
65
21
|
```
|
|
66
22
|
|
|
67
|
-
|
|
23
|
+
Run `/myaidev-method:configure payloadcms` to set these up.
|
|
68
24
|
|
|
69
|
-
|
|
70
|
-
|-------------|-----------------|
|
|
71
|
-
| title | title |
|
|
72
|
-
| meta_description | meta.description |
|
|
73
|
-
| slug | slug |
|
|
74
|
-
| tags | tags (relationship) |
|
|
75
|
-
| category | category (relationship) |
|
|
76
|
-
| featured_image | featuredImage (upload) |
|
|
25
|
+
## Publishing workflow
|
|
77
26
|
|
|
78
|
-
|
|
27
|
+
Follow these steps in order.
|
|
28
|
+
|
|
29
|
+
### Step 1: Read and validate the source file
|
|
79
30
|
|
|
80
|
-
|
|
81
|
-
- Credentials configured via `/configure payloadcms`
|
|
82
|
-
- Target collection must exist in PayloadCMS schema
|
|
31
|
+
Read the markdown file. Verify it has YAML frontmatter with at least a `title`. Stop if missing.
|
|
83
32
|
|
|
84
|
-
|
|
33
|
+
### Step 2: Auto-populate missing fields
|
|
85
34
|
|
|
86
|
-
|
|
87
|
-
- Collection not found → list available collections
|
|
88
|
-
- Validation error → show which fields failed
|
|
89
|
-
- Upload failure → publish without images, report which failed
|
|
35
|
+
Inspect the frontmatter. For any of these fields that are **missing or empty**, generate them from the content body and write them into the frontmatter before publishing:
|
|
90
36
|
|
|
91
|
-
|
|
37
|
+
| Field | How to generate | Max length |
|
|
38
|
+
|-------|----------------|------------|
|
|
39
|
+
| `slug` | Kebab-case from `title` | 80 chars |
|
|
40
|
+
| `excerpt` | Summarize the article in 1-2 sentences. Capture the core value proposition. | 160 chars |
|
|
41
|
+
| `meta.title` | SEO-optimized variation of `title`. Include primary keyword near the front. | 60 chars |
|
|
42
|
+
| `meta.description` | Compelling search snippet. Include primary keyword, end with implicit CTA. | 155 chars |
|
|
43
|
+
| `heroImage` | If the first element in the content body is a standalone image (`` on its own line), **promote** it: remove it from the body and note the path for hero image upload in Step 3. | |
|
|
44
|
+
|
|
45
|
+
**Do not overwrite** fields the user already set. Only fill gaps.
|
|
46
|
+
|
|
47
|
+
After generating, write the updated frontmatter back to the file, then re-read to confirm.
|
|
48
|
+
|
|
49
|
+
### Step 3: Upload hero image (if needed)
|
|
50
|
+
|
|
51
|
+
If Step 2 identified a hero image to promote, upload it before publishing:
|
|
92
52
|
|
|
93
|
-
Can invoke the publishing script for complex operations:
|
|
94
53
|
```bash
|
|
95
|
-
node
|
|
54
|
+
node --input-type=module -e "
|
|
55
|
+
import { PayloadCMSUtils } from '${CLAUDE_PLUGIN_ROOT}/src/lib/payloadcms-utils.js';
|
|
56
|
+
const u = new PayloadCMSUtils(); await u.authenticate();
|
|
57
|
+
const r = await u.uploadMedia('<hero-path>', '<hero-alt>');
|
|
58
|
+
console.log(JSON.stringify({ id: r.doc?.id || r.id }));
|
|
59
|
+
"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Substitute `<hero-path>` and `<hero-alt>` with the image path and alt text from the promoted image.
|
|
63
|
+
|
|
64
|
+
Then set `heroImage` in frontmatter to the returned media ID.
|
|
65
|
+
|
|
66
|
+
### Step 4: Dry run
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node "${CLAUDE_PLUGIN_ROOT}/src/scripts/payloadcms-publish.js" "<file>" --collection "<collection>" --status "<status>" --dry-run --json --verbose
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Defaults: collection=`posts`, status=`draft`. Parse stdout as JSON. If `"success": false`, use the [error recovery table](#error-recovery) and stop.
|
|
73
|
+
|
|
74
|
+
### Step 5: Publish
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node "${CLAUDE_PLUGIN_ROOT}/src/scripts/payloadcms-publish.js" "<file>" --collection "<collection>" --status "<status>" --json --verbose
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Add `--id <id>` if updating an existing document.
|
|
81
|
+
|
|
82
|
+
### Step 6: Report result
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
PayloadCMS Publish Result
|
|
86
|
+
Action: created | updated
|
|
87
|
+
Document: <document.id>
|
|
88
|
+
Collection: <document.collection>
|
|
89
|
+
Title: <document.title>
|
|
90
|
+
Auto-generated: <list any fields you populated in Step 2>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Script CLI reference
|
|
94
|
+
|
|
95
|
+
Script: `${CLAUDE_PLUGIN_ROOT}/src/scripts/payloadcms-publish.js`
|
|
96
|
+
|
|
97
|
+
| Flag | Description | Default |
|
|
98
|
+
|------|-------------|---------|
|
|
99
|
+
| `<file>` | Markdown file path (positional, or `--file`/`-f`) | Required |
|
|
100
|
+
| `--collection`, `-c` | Target collection | `posts` |
|
|
101
|
+
| `--status`, `-s` | `draft` or `published` | `draft` |
|
|
102
|
+
| `--id` | Document ID for updates | New document |
|
|
103
|
+
| `--dry-run` | Validate health + auth only | Off |
|
|
104
|
+
| `--json` | Machine-readable JSON on stdout | Off |
|
|
105
|
+
| `--verbose`, `-v` | Progress on stderr | Off |
|
|
106
|
+
|
|
107
|
+
**Always pass `--json --verbose`**.
|
|
108
|
+
|
|
109
|
+
## Markdown file format
|
|
110
|
+
|
|
111
|
+
```markdown
|
|
112
|
+
---
|
|
113
|
+
title: My Article Title
|
|
114
|
+
slug: my-article-title
|
|
115
|
+
excerpt: A brief summary for listing pages.
|
|
116
|
+
meta:
|
|
117
|
+
title: SEO Title - Primary Keyword
|
|
118
|
+
description: Compelling meta description with keyword and implicit CTA.
|
|
119
|
+
heroImage: 64a1b2c3d4e5f6
|
|
120
|
+
tags: [tag1, tag2]
|
|
121
|
+
category: tutorials
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
Your article content here with **bold**, *italic*, `code`, [links](https://...), lists, headings, and code blocks.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Any frontmatter key maps directly to a PayloadCMS field. See [references/field-mapping.md](references/field-mapping.md) for all supported fields and patterns (hero images, SEO, excerpts, glossary, relationships).
|
|
128
|
+
|
|
129
|
+
See [references/lexical-format.md](references/lexical-format.md) for Lexical conversion details and supported node types.
|
|
130
|
+
|
|
131
|
+
## Error recovery
|
|
132
|
+
|
|
133
|
+
| Error | Cause | Fix |
|
|
134
|
+
|-------|-------|-----|
|
|
135
|
+
| `Failed to load PayloadCMS configuration` | Missing `.env` | Run `/myaidev-method:configure payloadcms` |
|
|
136
|
+
| `PayloadCMS is not reachable` | Wrong URL or down | Check `PAYLOADCMS_URL` |
|
|
137
|
+
| `Authentication failed (401)` | Bad credentials | Check email/password |
|
|
138
|
+
| `HTTP 404` | Collection missing | Verify collection name |
|
|
139
|
+
| `HTTP 400` | Validation error | Check required fields against schema |
|
|
140
|
+
| `parseEditorState: type "X" not found` | Feature not enabled | Enable in Payload config: `BlocksFeature({ blocks: [CodeBlock()] })`, `ChecklistFeature()`, `TableFeature()` |
|
|
141
|
+
| `Media upload failed` | File not found or rejected | Check image paths relative to markdown file |
|
|
142
|
+
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Publish as draft (auto-generates missing SEO fields)
|
|
147
|
+
/myaidev-method:payloadcms-publisher article.md
|
|
148
|
+
|
|
149
|
+
# Publish immediately
|
|
150
|
+
/myaidev-method:payloadcms-publisher article.md --status published
|
|
151
|
+
|
|
152
|
+
# Different collection
|
|
153
|
+
/myaidev-method:payloadcms-publisher article.md --collection tutorials
|
|
154
|
+
|
|
155
|
+
# Update existing
|
|
156
|
+
/myaidev-method:payloadcms-publisher article.md --id 60d5ec49f8d2e
|
|
157
|
+
|
|
158
|
+
# Dry run
|
|
159
|
+
/myaidev-method:payloadcms-publisher article.md --dry-run
|
|
96
160
|
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# PayloadCMS Field Mapping
|
|
2
|
+
|
|
3
|
+
Frontmatter fields in the markdown source are mapped to PayloadCMS document fields by the publishing script.
|
|
4
|
+
|
|
5
|
+
## Standard fields
|
|
6
|
+
|
|
7
|
+
| Frontmatter Key | PayloadCMS Field | Type | Auto-populated | Notes |
|
|
8
|
+
|-----------------|-----------------|------|----------------|-------|
|
|
9
|
+
| `title` | `title` | string | No | Required — must be set by user |
|
|
10
|
+
| `slug` | `slug` | string | Yes | Kebab-case from title, max 80 chars |
|
|
11
|
+
| `excerpt` | `excerpt` | textarea | Yes | 1-2 sentence summary, max 160 chars |
|
|
12
|
+
| `meta.title` | `meta.title` | string | Yes | SEO title with primary keyword, max 60 chars |
|
|
13
|
+
| `meta.description` | `meta.description` | string | Yes | Search snippet with keyword + CTA, max 155 chars |
|
|
14
|
+
| `heroImage` | `heroImage` | upload | Yes | Promoted from first content image if standalone |
|
|
15
|
+
| `tags` | `tags` | relationship[] | No | Array of tag names or IDs |
|
|
16
|
+
| `category` | `category` | relationship | No | Category name or ID |
|
|
17
|
+
| `status` | `status` | string | No | Overridden by `--status` CLI flag |
|
|
18
|
+
|
|
19
|
+
**Auto-populated** fields are generated by Claude during the publishing workflow (Step 2) when missing from frontmatter. User-provided values always take precedence.
|
|
20
|
+
|
|
21
|
+
## How mapping works
|
|
22
|
+
|
|
23
|
+
The script uses `gray-matter` to parse YAML frontmatter, then spreads all frontmatter fields directly into the PayloadCMS document payload:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
---
|
|
27
|
+
title: My Post
|
|
28
|
+
customField: some value
|
|
29
|
+
nestedField:
|
|
30
|
+
subField: nested value
|
|
31
|
+
---
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Becomes:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"title": "My Post",
|
|
39
|
+
"customField": "some value",
|
|
40
|
+
"nestedField": { "subField": "nested value" },
|
|
41
|
+
"content": { "root": { ... } },
|
|
42
|
+
"status": "draft"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Any custom fields defined in your PayloadCMS collection schema can be set via frontmatter — the script passes them through as-is.
|
|
47
|
+
|
|
48
|
+
## Hero image (featured image)
|
|
49
|
+
|
|
50
|
+
PayloadCMS handles hero/featured images as a collection-level `upload` field — not inside the Lexical editor. This is the same pattern as WordPress featured images.
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
---
|
|
54
|
+
title: My Post
|
|
55
|
+
heroImage: 64a1b2c3d4e5f6 # media document ID
|
|
56
|
+
---
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The field name depends on your collection schema (common names: `heroImage`, `featuredImage`, `image`, `hero.media`). Provide the media ID directly. If you need to upload first, use the `uploadMedia()` utility and reference the returned ID.
|
|
60
|
+
|
|
61
|
+
For nested hero fields:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
---
|
|
65
|
+
hero:
|
|
66
|
+
type: highImpact
|
|
67
|
+
media: 64a1b2c3d4e5f6
|
|
68
|
+
---
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## SEO / Meta fields
|
|
72
|
+
|
|
73
|
+
If your PayloadCMS instance uses `@payloadcms/plugin-seo`, SEO fields are collection-level (not Lexical nodes). The plugin adds a `meta` group field:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
---
|
|
77
|
+
title: My Post
|
|
78
|
+
meta:
|
|
79
|
+
title: SEO Title Override
|
|
80
|
+
description: Meta description for search engines
|
|
81
|
+
image: 64a1b2c3d4e5f6 # media ID for og:image
|
|
82
|
+
---
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If your schema uses flat field names instead (no plugin), adjust accordingly:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
---
|
|
89
|
+
meta_title: SEO Title
|
|
90
|
+
meta_description: Meta description
|
|
91
|
+
---
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Excerpt
|
|
95
|
+
|
|
96
|
+
Excerpts are typically a plain `textarea` field on the collection, not part of the Lexical editor:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
---
|
|
100
|
+
title: My Post
|
|
101
|
+
excerpt: A brief summary of the article shown in listing pages and social cards.
|
|
102
|
+
---
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Glossary
|
|
106
|
+
|
|
107
|
+
PayloadCMS has no built-in glossary feature. Glossaries are implemented as either:
|
|
108
|
+
|
|
109
|
+
1. **A separate `glossary` collection** with `term` and `definition` fields — reference terms via a relationship field on your post:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
---
|
|
113
|
+
glossaryTerms: [term-id-1, term-id-2]
|
|
114
|
+
---
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
2. **A custom Lexical block** via `BlocksFeature` — these are rendered inside the rich text editor but require a custom block definition on the PayloadCMS instance. The markdown converter cannot produce custom blocks; they must be injected programmatically after conversion.
|
|
118
|
+
|
|
119
|
+
## Relationship fields
|
|
120
|
+
|
|
121
|
+
Tags and categories are typically relationship fields in PayloadCMS. Provide either:
|
|
122
|
+
- **Names** (if your schema has a `name` field): `tags: [javascript, tutorial]`
|
|
123
|
+
- **IDs** (for direct references): `tags: [abc123, def456]`
|
|
124
|
+
|
|
125
|
+
The exact behavior depends on your PayloadCMS collection schema configuration.
|
|
126
|
+
|
|
127
|
+
## Media uploads (in content body)
|
|
128
|
+
|
|
129
|
+
The publishing script automatically uploads images referenced in the markdown content body. During `publishContent()`:
|
|
130
|
+
|
|
131
|
+
1. `<div class="infographic" ...>` and `</div>` wrapper lines are stripped
|
|
132
|
+
2. Standalone `` image lines are detected
|
|
133
|
+
3. Each image file is uploaded to `/api/media` via `uploadMedia()`
|
|
134
|
+
4. The image reference is replaced with an `upload` Lexical node pointing to the media ID
|
|
135
|
+
|
|
136
|
+
Image paths are resolved relative to the markdown file's directory. If an image file doesn't exist or upload fails, a warning is logged and the image markdown is left as-is (rendered as text).
|
|
137
|
+
|
|
138
|
+
Media referenced in frontmatter fields (e.g. `heroImage`) is **not** auto-uploaded — provide a media ID directly for those fields.
|
|
139
|
+
|
|
140
|
+
## Inline images
|
|
141
|
+
|
|
142
|
+
PayloadCMS Lexical does **not** support inline images. The `UploadNode` is block-level only (`isInline()` returns `false`). Images in content are always rendered as standalone blocks between paragraphs, never inline with text. This is a Lexical architecture constraint, not a limitation of this script.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# PayloadCMS Lexical Rich Text Format
|
|
2
|
+
|
|
3
|
+
PayloadCMS uses the Lexical editor by default. The publishing script converts markdown to Lexical JSON automatically via `convertMarkdownToLexical()` in `payloadcms-utils.js`, which uses a headless Lexical editor with PayloadCMS's own node classes and markdown transformers from `@payloadcms/richtext-lexical`.
|
|
4
|
+
|
|
5
|
+
## Root structure
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"root": {
|
|
10
|
+
"type": "root",
|
|
11
|
+
"format": "",
|
|
12
|
+
"indent": 0,
|
|
13
|
+
"version": 1,
|
|
14
|
+
"children": [ ... ],
|
|
15
|
+
"direction": null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Supported node types
|
|
21
|
+
|
|
22
|
+
| Markdown | Lexical Node | Key Properties |
|
|
23
|
+
|----------|-------------|----------------|
|
|
24
|
+
| `# Heading` | `heading` | `tag: "h1"` through `"h6"` |
|
|
25
|
+
| Paragraph text | `paragraph` | `children: [text nodes]`, `textFormat`, `textStyle` |
|
|
26
|
+
| `- list item` | `list` + `listitem` | `listType: "bullet"` or `"number"`, `tag: "ul"` or `"ol"` |
|
|
27
|
+
| `> blockquote` | `quote` | Wraps text children directly |
|
|
28
|
+
| `---` | `horizontalrule` | DecoratorNode, no children |
|
|
29
|
+
| `[link](url)` | `link` | `fields: { url, linkType, newTab }`, `version: 3` |
|
|
30
|
+
| `\| col \| col \|` | `table` > `tablerow` > `tablecell` | `headerState: 0\|1`, `colSpan`, `rowSpan` |
|
|
31
|
+
| `` | `upload` | `relationTo: "media"`, `value: "<media-id>"`, `id: "<node-id>"`, `fields: {}`, `version: 3` |
|
|
32
|
+
| `` ```lang `` | `block` | `fields: { blockType: "Code", code, language, id }`, `version: 2` |
|
|
33
|
+
| `- [ ] item` | `list` + `listitem` | `listType: "check"`, `checked: true\|false` |
|
|
34
|
+
|
|
35
|
+
Code blocks (triple backticks) are converted to PayloadCMS `block` nodes with `blockType: "Code"` via the premade CodeBlock feature. The `language` field captures the language tag and `code` contains the block content. The PayloadCMS instance must have `BlocksFeature({ blocks: [CodeBlock()] })` enabled to render these blocks.
|
|
36
|
+
|
|
37
|
+
Checklists (`- [ ]` / `- [x]`) produce `list` nodes with `listType: "check"`. Each `listitem` has a `checked` boolean. The PayloadCMS instance must have `ChecklistFeature()` enabled.
|
|
38
|
+
|
|
39
|
+
Images (``) are handled during `publishContent()` — the file is uploaded to the `media` collection first, then an `upload` node referencing the media ID is injected into the Lexical JSON. Upload nodes require both an `id` (unique node identifier, not the media document ID) and a `fields` object (can be empty `{}`). Without these, the PayloadCMS frontend renderer fails to display the image in preview. Tables require the `EXPERIMENTAL_TableFeature` to be enabled on the PayloadCMS instance.
|
|
40
|
+
|
|
41
|
+
## Text formatting (bitwise flags)
|
|
42
|
+
|
|
43
|
+
Inline formatting uses Lexical's bitwise format codes:
|
|
44
|
+
|
|
45
|
+
| Format | Code | Markdown |
|
|
46
|
+
|--------|------|----------|
|
|
47
|
+
| Plain | `0` | `text` |
|
|
48
|
+
| Bold | `1` | `**text**` |
|
|
49
|
+
| Italic | `2` | `*text*` |
|
|
50
|
+
| Bold + Italic | `3` | `***text***` |
|
|
51
|
+
| Strikethrough | `4` | `~~text~~` |
|
|
52
|
+
| Underline | `8` | N/A |
|
|
53
|
+
| Code | `16` | `` `text` `` |
|
|
54
|
+
| Subscript | `32` | N/A |
|
|
55
|
+
| Superscript | `64` | N/A |
|
|
56
|
+
| Highlight | `128` | `==text==` |
|
|
57
|
+
|
|
58
|
+
Formats combine via bitwise OR. For example, bold italic code = `1 | 2 | 16 = 19`.
|
|
59
|
+
|
|
60
|
+
## Text node structure
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"type": "text",
|
|
65
|
+
"text": "Hello world",
|
|
66
|
+
"format": 0,
|
|
67
|
+
"mode": "normal",
|
|
68
|
+
"style": "",
|
|
69
|
+
"detail": 0,
|
|
70
|
+
"version": 1
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Coverage: markdown-producible vs non-markdown nodes
|
|
75
|
+
|
|
76
|
+
The converter handles **every Lexical node type that can be produced from markdown input**. The table above is the complete set.
|
|
77
|
+
|
|
78
|
+
PayloadCMS's default Lexical editor also registers these node types, which are **not producible from markdown** and therefore not part of this conversion pipeline:
|
|
79
|
+
|
|
80
|
+
| Node | Purpose | How to use |
|
|
81
|
+
|------|---------|------------|
|
|
82
|
+
| `relationship` | Embed a reference to another collection document | Programmatic only — use `RelationshipServerNode` |
|
|
83
|
+
| `autolink` | Auto-detected URLs in editor typing | Client-side only — never produced from markdown |
|
|
84
|
+
|
|
85
|
+
These nodes exist for interactive editor use and API round-tripping, not markdown conversion.
|
|
86
|
+
|
|
87
|
+
### What about hero images, SEO, excerpts?
|
|
88
|
+
|
|
89
|
+
These are **collection-level fields**, not Lexical editor nodes. They're set via frontmatter and passed through to the PayloadCMS API payload. See [field-mapping.md](field-mapping.md) for details.
|
|
90
|
+
|
|
91
|
+
### Inline images
|
|
92
|
+
|
|
93
|
+
PayloadCMS Lexical does not support inline images. `UploadNode` is block-level only. Images are always rendered as standalone blocks between paragraphs.
|
|
94
|
+
|
|
95
|
+
## Validation
|
|
96
|
+
|
|
97
|
+
The converter uses Lexical's own serialization (`editor.getEditorState().toJSON()`), so the output is guaranteed to be structurally valid. No separate validation step is needed — the JSON is identical to what PayloadCMS's editor produces.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-auditor
|
|
3
3
|
description: Performs security compliance audits and code security reviews against industry standards (OWASP, SOC2, HIPAA, PCI-DSS). Use when auditing code for security vulnerabilities, checking compliance, or generating security reports.
|
|
4
|
-
argument-hint: [path] [--standard=owasp]
|
|
4
|
+
argument-hint: "[path] [--standard=owasp]"
|
|
5
5
|
allowed-tools: [Read, Glob, Grep, Task, WebSearch]
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -228,17 +228,21 @@ async function installSkill(name, options) {
|
|
|
228
228
|
const slug = skill.slug || skill.name.toLowerCase().replace(/\s+/g, '-');
|
|
229
229
|
const installPath = path.join(targetDir, slug);
|
|
230
230
|
|
|
231
|
-
// Download
|
|
231
|
+
// Download (request JSON to get supporting files if available)
|
|
232
232
|
const dlSpinner = ora(`Downloading ${chalk.cyan(skill.name)}...`).start();
|
|
233
|
-
const dlRes = await fetchWithTimeout(`${API_BASE}/${skill.id}/download
|
|
233
|
+
const dlRes = await fetchWithTimeout(`${API_BASE}/${skill.id}/download`, {
|
|
234
|
+
headers: { 'Accept': 'application/json' },
|
|
235
|
+
});
|
|
234
236
|
if (!dlRes.ok) throw new Error(`Download failed: ${dlRes.status}`);
|
|
235
237
|
|
|
236
238
|
// Handle both JSON and raw markdown responses
|
|
237
239
|
const contentType = dlRes.headers.get('content-type') || '';
|
|
238
240
|
let content;
|
|
241
|
+
let supportingFiles = null;
|
|
239
242
|
if (contentType.includes('application/json')) {
|
|
240
243
|
const dlBody = await dlRes.json();
|
|
241
244
|
content = dlBody.content;
|
|
245
|
+
supportingFiles = dlBody.supportingFiles || null;
|
|
242
246
|
} else {
|
|
243
247
|
content = await dlRes.text();
|
|
244
248
|
}
|
|
@@ -248,10 +252,41 @@ async function installSkill(name, options) {
|
|
|
248
252
|
await fs.ensureDir(installPath);
|
|
249
253
|
await fs.writeFile(path.join(installPath, 'SKILL.md'), content, 'utf8');
|
|
250
254
|
|
|
255
|
+
// Install supporting files (agents, scripts, references, assets)
|
|
256
|
+
let extraFileCount = 0;
|
|
257
|
+
if (supportingFiles && typeof supportingFiles === 'object') {
|
|
258
|
+
for (const [relPath, fileContent] of Object.entries(supportingFiles)) {
|
|
259
|
+
const filePath = path.join(installPath, relPath);
|
|
260
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
261
|
+
await fs.writeFile(filePath, fileContent, 'utf8');
|
|
262
|
+
extraFileCount++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
251
266
|
dlSpinner.succeed(chalk.green(`Installed ${chalk.bold(skill.name)}`));
|
|
252
267
|
|
|
268
|
+
// Track installation (non-blocking, best-effort)
|
|
269
|
+
try {
|
|
270
|
+
const token = await getAuthToken();
|
|
271
|
+
if (token) {
|
|
272
|
+
fetchWithTimeout(`${API_BASE}/${skill.id}/install`, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
'Authorization': `Bearer ${token}`,
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify({ source: 'cli' }),
|
|
279
|
+
}).catch(() => {}); // Fire-and-forget
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Silently ignore tracking failures
|
|
283
|
+
}
|
|
284
|
+
|
|
253
285
|
const rel = path.relative(process.cwd(), installPath);
|
|
254
286
|
console.log(chalk.gray(` → ${rel}/SKILL.md`));
|
|
287
|
+
if (extraFileCount > 0) {
|
|
288
|
+
console.log(chalk.gray(` → ${extraFileCount} supporting file(s) (agents, scripts, etc.)`));
|
|
289
|
+
}
|
|
255
290
|
console.log(chalk.cyan('\n Restart Claude Code to load the new skill.\n'));
|
|
256
291
|
} catch (err) {
|
|
257
292
|
if (err.name === 'AbortError') {
|
|
@@ -782,18 +817,48 @@ async function submitSkill(opts) {
|
|
|
782
817
|
}
|
|
783
818
|
}
|
|
784
819
|
|
|
785
|
-
// Step 5:
|
|
820
|
+
// Step 5: Detect supporting files (agents, scripts, references, assets)
|
|
821
|
+
const supportingDirs = ['agents', 'scripts', 'references', 'assets', 'evals'];
|
|
822
|
+
const bundledFiles = {};
|
|
823
|
+
let hasSupportingFiles = false;
|
|
824
|
+
|
|
825
|
+
for (const dir of supportingDirs) {
|
|
826
|
+
const dirPath = path.join(skillDir, dir);
|
|
827
|
+
if (await fs.pathExists(dirPath)) {
|
|
828
|
+
const files = await collectFiles(dirPath, dir);
|
|
829
|
+
if (files.length > 0) {
|
|
830
|
+
hasSupportingFiles = true;
|
|
831
|
+
for (const f of files) {
|
|
832
|
+
bundledFiles[f.relativePath] = f.content;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (hasSupportingFiles) {
|
|
839
|
+
const fileCount = Object.keys(bundledFiles).length;
|
|
840
|
+
console.log(chalk.gray(` Bundling ${fileCount} supporting file(s) (agents, scripts, etc.)\n`));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Step 6: Submit via API
|
|
786
844
|
const submitSpinner = ora('Submitting skill for review...').start();
|
|
787
845
|
|
|
788
846
|
try {
|
|
789
847
|
const content = await fs.readFile(skillFile, 'utf8');
|
|
790
848
|
|
|
849
|
+
const payload = {
|
|
850
|
+
content,
|
|
851
|
+
category: result.info.category || undefined,
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Include supporting files if present
|
|
855
|
+
if (hasSupportingFiles) {
|
|
856
|
+
payload.supportingFiles = bundledFiles;
|
|
857
|
+
}
|
|
858
|
+
|
|
791
859
|
const res = await authFetch(`${API_BASE}/submissions`, {
|
|
792
860
|
method: 'POST',
|
|
793
|
-
body: JSON.stringify(
|
|
794
|
-
content,
|
|
795
|
-
category: result.info.category || undefined,
|
|
796
|
-
}),
|
|
861
|
+
body: JSON.stringify(payload),
|
|
797
862
|
});
|
|
798
863
|
|
|
799
864
|
if (!res.ok) {
|
|
@@ -816,6 +881,9 @@ async function submitSkill(opts) {
|
|
|
816
881
|
console.log('');
|
|
817
882
|
console.log(` ${chalk.gray('Submission ID:')} ${chalk.white(submission.id)}`);
|
|
818
883
|
console.log(` ${chalk.gray('Status:')} ${chalk.yellow(submission.status)}`);
|
|
884
|
+
if (hasSupportingFiles) {
|
|
885
|
+
console.log(` ${chalk.gray('Files:')} ${chalk.white(`SKILL.md + ${Object.keys(bundledFiles).length} supporting files`)}`);
|
|
886
|
+
}
|
|
819
887
|
console.log('');
|
|
820
888
|
console.log(chalk.gray(' AI analysis will run automatically.'));
|
|
821
889
|
console.log(chalk.gray(' An admin will review and approve your skill.'));
|
|
@@ -826,6 +894,36 @@ async function submitSkill(opts) {
|
|
|
826
894
|
}
|
|
827
895
|
}
|
|
828
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Recursively collect files from a directory for bundling.
|
|
899
|
+
* Skips __pycache__, .pyc, and other artifacts.
|
|
900
|
+
*/
|
|
901
|
+
async function collectFiles(dirPath, prefix) {
|
|
902
|
+
const results = [];
|
|
903
|
+
const SKIP_PATTERNS = ['__pycache__', '.pyc', '.DS_Store', 'node_modules'];
|
|
904
|
+
|
|
905
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
906
|
+
for (const entry of entries) {
|
|
907
|
+
if (SKIP_PATTERNS.some(p => entry.name.includes(p))) continue;
|
|
908
|
+
|
|
909
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
910
|
+
const relativePath = path.join(prefix, entry.name);
|
|
911
|
+
|
|
912
|
+
if (entry.isDirectory()) {
|
|
913
|
+
const subFiles = await collectFiles(fullPath, relativePath);
|
|
914
|
+
results.push(...subFiles);
|
|
915
|
+
} else if (entry.isFile()) {
|
|
916
|
+
try {
|
|
917
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
918
|
+
results.push({ relativePath, content });
|
|
919
|
+
} catch {
|
|
920
|
+
// Skip binary files
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return results;
|
|
925
|
+
}
|
|
926
|
+
|
|
829
927
|
// ── Status Command ─────────────────────────────────────────────────────
|
|
830
928
|
|
|
831
929
|
async function checkStatus(nameOrId) {
|