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.
Files changed (94) hide show
  1. package/.claude-plugin/plugin.json +0 -1
  2. package/.env.example +5 -4
  3. package/CHANGELOG.md +2 -2
  4. package/CONTENT_CREATION_GUIDE.md +489 -3211
  5. package/DEVELOPER_USE_CASES.md +1 -1
  6. package/MODULAR_INSTALLATION.md +2 -2
  7. package/README.md +39 -33
  8. package/TECHNICAL_ARCHITECTURE.md +1 -1
  9. package/USER_GUIDE.md +242 -190
  10. package/agents/content-editor-agent.md +90 -0
  11. package/agents/content-planner-agent.md +97 -0
  12. package/agents/content-research-agent.md +62 -0
  13. package/agents/content-seo-agent.md +101 -0
  14. package/agents/content-writer-agent.md +69 -0
  15. package/agents/infographic-analyzer-agent.md +63 -0
  16. package/agents/infographic-designer-agent.md +72 -0
  17. package/bin/cli.js +776 -422
  18. package/{content-rules.example.md → content-rules-example.md} +2 -2
  19. package/dist/mcp/health-check.js +82 -68
  20. package/dist/mcp/mcp-config.json +8 -0
  21. package/dist/mcp/openstack-server.js +1746 -1262
  22. package/dist/server/.tsbuildinfo +1 -1
  23. package/extension.json +21 -4
  24. package/package.json +181 -184
  25. package/skills/company-config/SKILL.md +133 -0
  26. package/skills/configure/SKILL.md +1 -1
  27. package/skills/myai-configurator/SKILL.md +77 -0
  28. package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
  29. package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
  30. package/skills/myai-content-enrichment/SKILL.md +114 -0
  31. package/skills/myai-content-ideation/SKILL.md +288 -0
  32. package/skills/myai-content-ideation/evals/evals.json +182 -0
  33. package/skills/myai-content-production-coordinator/SKILL.md +946 -0
  34. package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
  35. package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
  36. package/skills/myai-content-writer/SKILL.md +333 -0
  37. package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
  38. package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
  39. package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
  40. package/skills/myai-skill-builder/SKILL.md +699 -0
  41. package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
  42. package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
  43. package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
  44. package/skills/myai-skill-builder/assets/eval_review.html +131 -0
  45. package/skills/myai-skill-builder/references/schemas.md +211 -0
  46. package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
  47. package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
  48. package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
  49. package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
  50. package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
  51. package/skills/myai-skill-builder/scripts/utils.py +123 -0
  52. package/skills/myai-visual-generator/SKILL.md +125 -0
  53. package/skills/myai-visual-generator/evals/evals.json +155 -0
  54. package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
  55. package/skills/myai-visual-generator/references/research-visuals.md +57 -0
  56. package/skills/myai-visual-generator/references/services.md +89 -0
  57. package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
  58. package/skills/myaidev-figma/SKILL.md +212 -0
  59. package/skills/myaidev-figma/capture.js +133 -0
  60. package/skills/myaidev-figma/crawl.js +130 -0
  61. package/skills/myaidev-figma-configure/SKILL.md +130 -0
  62. package/skills/openstack-manager/SKILL.md +1 -1
  63. package/skills/payloadcms-publisher/SKILL.md +141 -77
  64. package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
  65. package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
  66. package/skills/security-auditor/SKILL.md +1 -1
  67. package/src/cli/commands/addon.js +105 -7
  68. package/src/config/workflows.js +172 -228
  69. package/src/lib/ascii-banner.js +197 -182
  70. package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
  71. package/src/lib/installation-detector.js +93 -59
  72. package/src/lib/payloadcms-utils.js +285 -510
  73. package/src/lib/workflow-installer.js +55 -0
  74. package/src/mcp/health-check.js +82 -68
  75. package/src/mcp/openstack-server.js +1746 -1262
  76. package/src/scripts/configure-visual-apis.js +224 -173
  77. package/src/scripts/configure-wordpress-mcp.js +96 -66
  78. package/src/scripts/init/install.js +109 -85
  79. package/src/scripts/init-project.js +138 -67
  80. package/src/scripts/utils/write-content.js +67 -52
  81. package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
  82. package/src/templates/claude/CLAUDE.md +19 -12
  83. package/hooks/hooks.json +0 -26
  84. package/skills/content-coordinator/SKILL.md +0 -130
  85. package/skills/content-enrichment/SKILL.md +0 -80
  86. package/skills/content-writer/SKILL.md +0 -285
  87. package/skills/skill-builder/SKILL.md +0 -417
  88. package/skills/visual-generator/SKILL.md +0 -140
  89. /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
  90. /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
  91. /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
  92. /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
  93. /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
  94. /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 handling. Use when publishing content to a PayloadCMS instance.
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, Write, Edit, Bash, Glob, Grep]
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
- You are a **PayloadCMS Publishing Agent** converting markdown content to PayloadCMS Lexical rich text format and publishing via the REST API.
12
-
13
- ## Arguments
14
-
15
- - `[file.md]` → Source markdown file (required)
16
- - `--status draft|published` → Publish status (default: draft)
17
- - `--collection <name>` → Target collection (default: posts)
18
- - `--id <doc-id>` → Update existing document
19
- - `--dry-run` → Validate without publishing
20
- - `--verbose` → Show detailed progress
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
- ## Field Mapping
23
+ Run `/myaidev-method:configure payloadcms` to set these up.
68
24
 
69
- | Frontmatter | PayloadCMS Field |
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
- ## Prerequisites
27
+ Follow these steps in order.
28
+
29
+ ### Step 1: Read and validate the source file
79
30
 
80
- - PayloadCMS instance with REST API enabled
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
- ## Error Handling
33
+ ### Step 2: Auto-populate missing fields
85
34
 
86
- - Auth failure report credential issue, suggest `/configure payloadcms`
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
- ## Script Integration
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 (`![alt](path)` 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 .myaidev-method/scripts/payloadcms-publish.js "article.md" --status draft
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 `![alt](path)` 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
+ | `![alt](path)` | `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 (`![alt](path)`) 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: Submit via API
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) {