@supatent/skills 0.4.0 → 0.5.1

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
@@ -1,10 +1,10 @@
1
1
  # @supatent/skills
2
2
 
3
- Claude Code content authoring skills for [Supatent CMS](https://supatent.ai).
3
+ Claude Code and Codex CLI content authoring skills for [Supatent CMS](https://supatent.ai).
4
4
 
5
5
  ## What this does
6
6
 
7
- Installs AI-powered content authoring skills into your Claude Code environment. Once installed, Claude Code can create and manage CMS content through natural conversation — blog posts, landing pages, and more.
7
+ Installs AI-powered content authoring skills for Claude Code and Codex CLI. Once installed, agents can create and manage CMS content through natural conversation — blog posts, landing pages, and more.
8
8
 
9
9
  ## Quick start
10
10
 
@@ -12,15 +12,40 @@ Installs AI-powered content authoring skills into your Claude Code environment.
12
12
  npx @supatent/skills
13
13
  ```
14
14
 
15
- This installs skill files to `.claude/skills/` in your project. Then use `/supatent:core` in Claude Code to get started.
15
+ In interactive terminals, the installer prompts for a target:
16
+
17
+ - `claude`
18
+ - `codex`
19
+ - `both`
20
+
21
+ In non-interactive environments (CI), the default is `claude`.
22
+
23
+ ### Explicit target selection
24
+
25
+ ```bash
26
+ # Claude Code
27
+ npx @supatent/skills --target claude
28
+
29
+ # Codex CLI
30
+ npx @supatent/skills --target codex
31
+
32
+ # Both
33
+ npx @supatent/skills --target both
34
+ ```
35
+
36
+ Use `--codex-home` to override Codex install location (default: `$CODEX_HOME` or `~/.codex`):
37
+
38
+ ```bash
39
+ npx @supatent/skills --target codex --codex-home /path/to/codex-home
40
+ ```
16
41
 
17
42
  ## Included skills
18
43
 
19
- | Skill | Command | Description |
20
- |-------|---------|-------------|
21
- | Core | `/supatent:core` | Schema management, content operations, validation |
22
- | Blog | `/supatent:blog` | Create and edit blog posts with structured fields |
23
- | Landing | `/supatent:landing` | Build landing pages with configurable sections |
44
+ | Skill | Claude Code | Codex CLI | Description |
45
+ |-------|-------------|-----------|-------------|
46
+ | Core | `/supatent:core` | `supatent-core` | Schema management, content operations, validation |
47
+ | Blog | `/supatent:content-blog` | `supatent-content-blog` | Create and edit blog posts with structured fields |
48
+ | Landing | `/supatent:content-landing` | `supatent-content-landing` | Build landing pages with configurable sections |
24
49
 
25
50
  ## Updating
26
51
 
@@ -36,7 +61,7 @@ npx @supatent/skills --force
36
61
 
37
62
  - Node.js >= 20
38
63
  - A Supatent CMS project with `@supatent/cli` configured
39
- - Claude Code
64
+ - Claude Code and/or Codex CLI
40
65
 
41
66
  ## License
42
67
 
package/bin/install.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/install.mjs - Self-contained zero-dependency installer for @supatent/skills
4
- // Copies bundled skill files to .claude/skills/ with manifest tracking.
4
+ // Copies bundled skill files to Claude Code and/or Codex CLI skill directories.
5
5
 
6
6
  import { readFile, writeFile, mkdir, copyFile, readdir, stat } from 'node:fs/promises';
7
7
  import { realpathSync } from 'node:fs';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { join, dirname, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { homedir } from 'node:os';
11
12
  import { createInterface } from 'node:readline/promises';
12
13
 
13
14
  // ---------------------------------------------------------------------------
@@ -16,8 +17,10 @@ import { createInterface } from 'node:readline/promises';
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
19
- const SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
20
- const TARGET_SUBDIR = join('.claude', 'skills');
20
+ const CLAUDE_SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
21
+ const CODEX_SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills-codex');
22
+ const CLAUDE_TARGET_SUBDIR = join('.claude', 'skills');
23
+ const VALID_TARGETS = new Set(['claude', 'codex', 'both']);
21
24
 
22
25
  // ---------------------------------------------------------------------------
23
26
  // Exported helpers (for testability)
@@ -65,6 +68,109 @@ export async function confirm(message, { force = false } = {}) {
65
68
  }
66
69
  }
67
70
 
71
+ /**
72
+ * Prompt install target when not explicitly provided.
73
+ * Defaults to "claude" when stdin is non-interactive.
74
+ */
75
+ export async function promptInstallTarget({ force = false } = {}) {
76
+ if (force || !process.stdin.isTTY) return 'claude';
77
+
78
+ const rl = createInterface({
79
+ input: process.stdin,
80
+ output: process.stdout,
81
+ });
82
+
83
+ try {
84
+ console.log('Select installation target:');
85
+ console.log(' 1) Claude Code');
86
+ console.log(' 2) Codex CLI');
87
+ console.log(' 3) Both');
88
+
89
+ while (true) {
90
+ const answer = (await rl.question('Target [1/2/3] (default: 1): ')).trim().toLowerCase();
91
+
92
+ if (answer === '' || answer === '1' || answer === 'claude') {
93
+ return 'claude';
94
+ }
95
+ if (answer === '2' || answer === 'codex') {
96
+ return 'codex';
97
+ }
98
+ if (answer === '3' || answer === 'both') {
99
+ return 'both';
100
+ }
101
+
102
+ console.log('Invalid choice. Enter 1, 2, 3, claude, codex, or both.');
103
+ }
104
+ } finally {
105
+ rl.close();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Parse command line args.
111
+ */
112
+ export function parseArgs(argv) {
113
+ const parsed = {
114
+ force: false,
115
+ help: false,
116
+ target: null,
117
+ codexHome: null,
118
+ };
119
+
120
+ for (let i = 0; i < argv.length; i += 1) {
121
+ const arg = argv[i];
122
+
123
+ if (arg === '--force' || arg === '-f' || arg === '--yes' || arg === '-y') {
124
+ parsed.force = true;
125
+ continue;
126
+ }
127
+
128
+ if (arg === '--help' || arg === '-h') {
129
+ parsed.help = true;
130
+ continue;
131
+ }
132
+
133
+ if (arg === '--target') {
134
+ i += 1;
135
+ if (!argv[i]) {
136
+ throw new Error('Missing value for --target. Use: claude, codex, or both.');
137
+ }
138
+ parsed.target = argv[i];
139
+ continue;
140
+ }
141
+
142
+ if (arg.startsWith('--target=')) {
143
+ parsed.target = arg.slice('--target='.length);
144
+ continue;
145
+ }
146
+
147
+ if (arg === '--codex-home') {
148
+ i += 1;
149
+ if (!argv[i]) {
150
+ throw new Error('Missing value for --codex-home.');
151
+ }
152
+ parsed.codexHome = argv[i];
153
+ continue;
154
+ }
155
+
156
+ if (arg.startsWith('--codex-home=')) {
157
+ parsed.codexHome = arg.slice('--codex-home='.length);
158
+ continue;
159
+ }
160
+
161
+ throw new Error(`Unknown argument: ${arg}`);
162
+ }
163
+
164
+ if (parsed.target) {
165
+ parsed.target = parsed.target.toLowerCase();
166
+ if (!VALID_TARGETS.has(parsed.target)) {
167
+ throw new Error(`Invalid --target value: ${parsed.target}. Use: claude, codex, or both.`);
168
+ }
169
+ }
170
+
171
+ return parsed;
172
+ }
173
+
68
174
  /**
69
175
  * Read and parse .manifest.json. Returns null if missing or invalid.
70
176
  */
@@ -161,60 +267,76 @@ export async function getVersion() {
161
267
  return pkg.version;
162
268
  }
163
269
 
164
- // ---------------------------------------------------------------------------
165
- // Main installer flow
166
- // ---------------------------------------------------------------------------
167
-
168
- async function main() {
169
- const args = process.argv.slice(2);
170
- const force = args.includes('--force');
270
+ /**
271
+ * Resolve default Codex home.
272
+ */
273
+ export function getDefaultCodexHome() {
274
+ return process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME) : join(homedir(), '.codex');
275
+ }
171
276
 
172
- const version = await getVersion();
173
- const cwd = process.cwd();
174
- const claudeDir = join(cwd, '.claude');
175
- const targetDir = join(cwd, TARGET_SUBDIR);
176
- const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
277
+ function printHelp() {
278
+ console.log('Usage: npx @supatent/skills [options]');
279
+ console.log('');
280
+ console.log('Options:');
281
+ console.log(' --target <claude|codex|both> Installation target (prompts when omitted in TTY)');
282
+ console.log(' --codex-home <path> Override Codex home (default: $CODEX_HOME or ~/.codex)');
283
+ console.log(' -f, --force Auto-confirm prompts and overwrite user-modified files');
284
+ console.log(' -y, --yes Alias for --force');
285
+ console.log(' -h, --help Show this help');
286
+ }
177
287
 
178
- // -----------------------------------------------------------------------
179
- // Step a: Check .claude/ directory
180
- // -----------------------------------------------------------------------
181
- let claudeExists = false;
288
+ async function ensureDirectoryExists(dirPath, missingMessage, { force }) {
289
+ let exists = false;
182
290
  try {
183
- const s = await stat(claudeDir);
184
- claudeExists = s.isDirectory();
291
+ const s = await stat(dirPath);
292
+ exists = s.isDirectory();
185
293
  } catch {
186
294
  // does not exist
187
295
  }
188
296
 
189
- if (!claudeExists) {
190
- console.log('\x1b[33m!\x1b[0m No .claude/ directory found. This installer creates skill files for Claude Code.');
191
- const ok = await confirm('Create .claude/ directory and continue?', { force });
192
- if (!ok) {
193
- console.log('Aborted.');
194
- process.exitCode = 1;
195
- return;
196
- }
197
- await mkdir(claudeDir, { recursive: true });
297
+ if (exists) return true;
298
+
299
+ if (missingMessage) {
300
+ console.log(`\x1b[33m!\x1b[0m ${missingMessage}`);
198
301
  }
199
302
 
303
+ const ok = await confirm(`Create ${dirPath} and continue?`, { force });
304
+ if (!ok) {
305
+ console.log('Aborted.');
306
+ return false;
307
+ }
308
+
309
+ await mkdir(dirPath, { recursive: true });
310
+ return true;
311
+ }
312
+
313
+ async function installSkillBundle({
314
+ sourceDir,
315
+ targetDir,
316
+ manifestPath,
317
+ targetLabel,
318
+ startHint,
319
+ force,
320
+ version,
321
+ }) {
200
322
  // -----------------------------------------------------------------------
201
- // Step b: Walk source skills/ directory and build source map
323
+ // Step a: Build source map
202
324
  // -----------------------------------------------------------------------
203
- const sourceMap = await buildSourceMap(SKILLS_SOURCE_DIR);
325
+ const sourceMap = await buildSourceMap(sourceDir);
204
326
  const sourceFiles = Object.keys(sourceMap);
205
327
 
206
328
  // -----------------------------------------------------------------------
207
- // Step c: Read existing manifest
329
+ // Step b: Read existing manifest
208
330
  // -----------------------------------------------------------------------
209
331
  const manifest = await readManifest(manifestPath);
210
332
 
211
333
  // -----------------------------------------------------------------------
212
- // Step d: Compute diff
334
+ // Step c: Compute diff
213
335
  // -----------------------------------------------------------------------
214
336
  const diff = await computeDiff(sourceMap, manifest, targetDir);
215
337
 
216
338
  // -----------------------------------------------------------------------
217
- // Step e: Handle user-modified files
339
+ // Step d: Handle user-modified files
218
340
  // -----------------------------------------------------------------------
219
341
  const filesToCopy = [...diff.newFiles, ...diff.changed];
220
342
  const skippedFiles = [];
@@ -232,14 +354,14 @@ async function main() {
232
354
  // Check if anything to do
233
355
  // -----------------------------------------------------------------------
234
356
  if (filesToCopy.length === 0 && skippedFiles.length === 0) {
235
- console.log(`\x1b[32m\u2713\x1b[0m Already up to date (v${version})`);
357
+ console.log(`\x1b[32m\u2713\x1b[0m Already up to date (${targetLabel}, v${version})`);
236
358
  console.log('');
237
- console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
359
+ console.log(` ${startHint}`);
238
360
  return;
239
361
  }
240
362
 
241
363
  // -----------------------------------------------------------------------
242
- // Step f: Copy files
364
+ // Step e: Copy files
243
365
  // -----------------------------------------------------------------------
244
366
  const isFreshInstall = !manifest;
245
367
 
@@ -263,7 +385,7 @@ async function main() {
263
385
  }
264
386
 
265
387
  // -----------------------------------------------------------------------
266
- // Step g: Write manifest
388
+ // Step f: Write manifest
267
389
  // -----------------------------------------------------------------------
268
390
  const manifestFiles = {};
269
391
 
@@ -293,16 +415,103 @@ async function main() {
293
415
  await writeFile(manifestPath, JSON.stringify(manifestData, null, 2) + '\n');
294
416
 
295
417
  // -----------------------------------------------------------------------
296
- // Step h: Print summary
418
+ // Step g: Print summary
297
419
  // -----------------------------------------------------------------------
298
420
  console.log('');
299
421
  if (isFreshInstall) {
300
- console.log(`\x1b[32m\u2713\x1b[0m Installed ${filesToCopy.length} files to ${TARGET_SUBDIR}/ (v${version})`);
422
+ console.log(`\x1b[32m\u2713\x1b[0m Installed ${filesToCopy.length} files to ${targetDir} (${targetLabel}, v${version})`);
301
423
  } else {
302
- console.log(`\x1b[32m\u2713\x1b[0m Updated ${filesToCopy.length} file${filesToCopy.length === 1 ? '' : 's'} in ${TARGET_SUBDIR}/ (v${version})`);
424
+ console.log(`\x1b[32m\u2713\x1b[0m Updated ${filesToCopy.length} file${filesToCopy.length === 1 ? '' : 's'} in ${targetDir} (${targetLabel}, v${version})`);
303
425
  }
304
426
  console.log('');
305
- console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
427
+ console.log(` ${startHint}`);
428
+ }
429
+
430
+ async function installClaude({ cwd, force, version }) {
431
+ const claudeDir = join(cwd, '.claude');
432
+ const targetDir = join(cwd, CLAUDE_TARGET_SUBDIR);
433
+ const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
434
+
435
+ const ok = await ensureDirectoryExists(
436
+ claudeDir,
437
+ 'No .claude/ directory found. This installer creates skill files for Claude Code.',
438
+ { force },
439
+ );
440
+ if (!ok) {
441
+ process.exitCode = 1;
442
+ return;
443
+ }
444
+
445
+ await installSkillBundle({
446
+ sourceDir: CLAUDE_SKILLS_SOURCE_DIR,
447
+ targetDir,
448
+ manifestPath,
449
+ targetLabel: 'Claude Code',
450
+ startHint: 'Run \x1b[36m/supatent:core\x1b[0m to get started',
451
+ force,
452
+ version,
453
+ });
454
+ }
455
+
456
+ async function installCodex({ force, version, codexHomeOverride = null }) {
457
+ const codexHome = codexHomeOverride ? resolve(codexHomeOverride) : getDefaultCodexHome();
458
+ const targetDir = join(codexHome, 'skills');
459
+ const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
460
+
461
+ const ok = await ensureDirectoryExists(
462
+ codexHome,
463
+ `No Codex home directory found at ${codexHome}.`,
464
+ { force },
465
+ );
466
+ if (!ok) {
467
+ process.exitCode = 1;
468
+ return;
469
+ }
470
+
471
+ await installSkillBundle({
472
+ sourceDir: CODEX_SKILLS_SOURCE_DIR,
473
+ targetDir,
474
+ manifestPath,
475
+ targetLabel: 'Codex CLI',
476
+ startHint: 'Restart Codex CLI to pick up new skills',
477
+ force,
478
+ version,
479
+ });
480
+ }
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // Main installer flow
484
+ // ---------------------------------------------------------------------------
485
+
486
+ async function main() {
487
+ const args = parseArgs(process.argv.slice(2));
488
+
489
+ if (args.help) {
490
+ printHelp();
491
+ return;
492
+ }
493
+
494
+ const version = await getVersion();
495
+ const cwd = process.cwd();
496
+ const force = args.force;
497
+
498
+ const target = args.target || await promptInstallTarget({ force });
499
+
500
+ if (target === 'both') {
501
+ console.log('Installing Supatent skills for Claude Code...');
502
+ await installClaude({ cwd, force, version });
503
+ console.log('');
504
+ console.log('Installing Supatent skills for Codex CLI...');
505
+ await installCodex({ force, version, codexHomeOverride: args.codexHome });
506
+ return;
507
+ }
508
+
509
+ if (target === 'codex') {
510
+ await installCodex({ force, version, codexHomeOverride: args.codexHome });
511
+ return;
512
+ }
513
+
514
+ await installClaude({ cwd, force, version });
306
515
  }
307
516
 
308
517
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@supatent/skills",
3
- "version": "0.4.0",
4
- "description": "Claude Code content authoring skills for Supatent CMS",
3
+ "version": "0.5.1",
4
+ "description": "Claude Code and Codex CLI content authoring skills for Supatent CMS",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "supatent-skills": "bin/install.mjs"
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "skills"
11
+ "skills",
12
+ "skills-codex"
12
13
  ],
13
14
  "devDependencies": {
14
15
  "eslint": "^9.16.0",
@@ -21,6 +22,7 @@
21
22
  "supatent",
22
23
  "cms",
23
24
  "claude-code",
25
+ "codex-cli",
24
26
  "skills",
25
27
  "content-authoring"
26
28
  ],
@@ -170,6 +170,7 @@ Generate a URL-friendly slug from the title (lowercase, hyphens, no special char
170
170
  **Body content:**
171
171
  - Default length: 1000-1500 words
172
172
  - Format: markdown with headings (H2, H3), paragraphs, lists, and emphasis as the content demands
173
+ - For Supatent assets in the `body`, use slug links: `![Alt](asset-slug)`, `[Link](asset-slug)`, or `<img src="asset-slug" ...>`
173
174
  - SEO-conscious writing: include target keywords in the first paragraph, use semantic keyword variations throughout, structure headings around searchable concepts
174
175
  - Content-driven structure: do NOT use a rigid template. Let the topic dictate whether the post needs numbered lists, narrative flow, comparison tables, or a mix.
175
176
  - Match the tone and style the user selected in the interview
@@ -243,6 +244,7 @@ After writing, check `.supatent/.validation-status.json` for errors. Common JSON
243
244
  |-------|-------|-----|
244
245
  | `unsupported @type` | Using BlogPosting instead of Article | Change `@type` to `"Article"` |
245
246
  | `image: expected uri format` | Using asset slug instead of URL | Remove `image` or use a full URL |
247
+ | `markdown references missing asset slug` | `body` contains an asset slug link with no matching local asset | Add/update `.supatent/assets/{slug}.{locale}.json` or change link target |
246
248
  | `required property 'name' is missing` | Author object missing name | Add `"name"` to the author Person object |
247
249
 
248
250
  ## Multi-Locale Translation
@@ -298,3 +300,4 @@ Format the guidance as:
298
300
  > Here are the images you will need for this post: [list with recommended sizes and format notes]
299
301
 
300
302
  Remind the user that images can be uploaded via the Supatent dashboard (drag-and-drop) or via the CLI asset upload flow. After uploading, set the `cover-image` field value to the asset slug returned by the upload.
303
+ For in-body assets, embed with slug syntax (`![Alt](asset-slug)` or `<img src="asset-slug" ...>`), not raw URLs.
@@ -240,6 +240,7 @@ Confirm these counts with the user during adaptive questions. Adjust if requeste
240
240
  - If context is too vague for a section, ask the user for more detail rather than generating filler
241
241
  - Match the tone preference from the interview throughout all sections
242
242
  - `features-list` on pricing tiers: use markdown bullet list format, one feature per line
243
+ - In markdown fields, reference Supatent assets by slug: `![Alt](asset-slug)`, `[Link](asset-slug)`, or `<img src="asset-slug" ...>`
243
244
  - `is-featured` on pricing tiers: set `"true"` on the recommended/most popular tier, `"false"` on others
244
245
  - Image fields: leave empty string `""` -- note images needed in the Image Guidance section
245
246
  - CTA URLs: use the website URL from the interview if provided, otherwise use placeholder `#`
@@ -331,6 +332,7 @@ After writing, check `.supatent/.validation-status.json` for errors. Common JSON
331
332
  |-------|-------|-----|
332
333
  | `unsupported @type` | Using WebPage instead of Organization | Change `@type` to `"Organization"` |
333
334
  | `image: expected uri format` | Using asset slug instead of URL | Remove `image` or use a full URL |
335
+ | `markdown references missing asset slug` | Markdown content references an unknown asset slug | Add/update `.supatent/assets/{slug}.{locale}.json` or change link target |
334
336
  | `mainEntity: required` | FAQPage missing Question array | Add at least one Question to `mainEntity` |
335
337
 
336
338
  ## Multi-Locale Translation
@@ -384,3 +386,4 @@ After content generation, provide a section-by-section image checklist. Only inc
384
386
  | Metadata | -- | -- | No images |
385
387
 
386
388
  Images can be uploaded via the Supatent dashboard (drag-and-drop) or the CLI asset upload flow. After uploading, set the image field value to the asset slug returned by the upload.
389
+ For markdown fields, embed/link those assets with slug syntax (`![Alt](asset-slug)` or `<img src="asset-slug" ...>`).
@@ -64,8 +64,9 @@ npm view @supatent/skills version 2>/dev/null
64
64
  |------|-----------|--------------|
65
65
  | `text` | `textInput`, `textarea` | Plain string |
66
66
  | `number` | `numberInput` | Numeric value |
67
- | `image` | `singleImage`, `multiImage` | Image reference object(s) |
68
- | `markdown` | `markdownEditor` | Markdown string |
67
+ | `image` | `singleImage`, `multiImage` | Asset slug string (`string`/`string[]`) or legacy object |
68
+ | `video` | `singleVideo`, `multiVideo` | Asset slug string (`string`/`string[]`) or legacy object |
69
+ | `markdown` | `markdownEditor` | Markdown string (supports asset slugs in links and image embeds) |
69
70
  | `jsonLd` | `jsonLdEditor` | JSON-LD structured data object |
70
71
 
71
72
  **Interface determines the UI editor**, not the storage format. For example, `text` with `textInput` gives a single-line input; `text` with `textarea` gives a multi-line editor.
@@ -169,6 +170,16 @@ Write a JSON file to `.supatent/content/{schemaSlug}/{itemSlug}.{locale}.json`:
169
170
 
170
171
  Field keys must match the field slugs defined in the schema. Each locale gets its own file (e.g., `my-post.en.json`, `my-post.fr.json`).
171
172
 
173
+ ### Markdown asset slug links
174
+
175
+ For markdown fields, use asset slugs directly when linking or embedding Supatent assets:
176
+
177
+ - `![Alt text](asset-slug)`
178
+ - `[Link text](asset-slug)`
179
+ - `<img src="asset-slug" width="1200" height="630" alt="...">`
180
+
181
+ Do not use file extensions or full API URLs when referencing Supatent assets in markdown. Missing slugs generate warning-only `markdown-asset` validation warnings.
182
+
172
183
  ### Validating
173
184
 
174
185
  If dev mode is running, validation happens automatically on file save. Otherwise:
@@ -215,7 +226,7 @@ The `interface` value is not valid for the given `type`. Each field type only su
215
226
 
216
227
  ### "Unknown field type"
217
228
 
218
- Only five field types are supported: `text`, `number`, `image`, `markdown`, `jsonLd`. Check for typos or capitalization errors (`jsonLd` not `jsonld` or `JsonLd`).
229
+ Six field types are supported: `text`, `number`, `image`, `video`, `markdown`, `jsonLd`. Check for typos or capitalization errors (`jsonLd` not `jsonld` or `JsonLd`).
219
230
 
220
231
  ### "Singleton must use default slug"
221
232
 
@@ -13,6 +13,8 @@ Every field in a schema has a `type` and an `interface`. The type determines the
13
13
  | `number` | `numberInput` | `number` or `null` | Numeric input; `null` represents empty (distinct from `0`) |
14
14
  | `image` | `singleImage` | `string` or `null` | Asset slug string (e.g., `"hero-image"`), or legacy object with `assetPath` |
15
15
  | `image` | `multiImage` | `string[]` | Array of asset slug strings (e.g., `["hero", "banner"]`) |
16
+ | `video` | `singleVideo` | `string` or `null` | Asset slug string (e.g., `"promo-video"`), or legacy object with `assetPath` |
17
+ | `video` | `multiVideo` | `string[]` | Array of asset slug strings (e.g., `["intro-video", "demo-video"]`) |
16
18
  | `markdown` | `markdownEditor` | `string` | Markdown-formatted text |
17
19
  | `jsonLd` | `jsonLdEditor` | `object` | JSON-LD object with `@context` and `@type` |
18
20
 
@@ -24,6 +26,7 @@ Every field in a schema has a `type` and an `interface`. The type determines the
24
26
  text -> textInput, textarea
25
27
  number -> numberInput
26
28
  image -> singleImage, multiImage
29
+ video -> singleVideo, multiVideo
27
30
  markdown -> markdownEditor
28
31
  jsonLd -> jsonLdEditor
29
32
  ```
@@ -75,7 +78,7 @@ Schema files live at `.supatent/schema/{slug}.json`. The complete structure:
75
78
  | `slug` | Yes | `string` | Unique within the schema. Lowercase alphanumeric with hyphens. |
76
79
  | `name` | Yes | `string` | Display name. Minimum 1 character. |
77
80
  | `description` | No | `string` | Optional description of the field's purpose. |
78
- | `type` | Yes | `string` | One of: `text`, `number`, `image`, `markdown`, `jsonLd` |
81
+ | `type` | Yes | `string` | One of: `text`, `number`, `image`, `video`, `markdown`, `jsonLd` |
79
82
  | `interface` | Yes | `string` | Must be valid for the field's type (see compatibility map above). |
80
83
  | `order` | Yes | `number` | Integer >= 0. Controls display order in the editor. |
81
84
 
@@ -163,9 +166,25 @@ The file is a flat JSON object where keys are field slugs from the schema:
163
166
  | `number` | `number` or `null` | `42` or `null` |
164
167
  | `image` (singleImage) | `string` (asset slug) or `null` | `"hero-image"` |
165
168
  | `image` (multiImage) | `string[]` (asset slugs) | `["photo-1", "photo-2"]` |
169
+ | `video` (singleVideo) | `string` (asset slug) or `null` | `"promo-video"` |
170
+ | `video` (multiVideo) | `string[]` (asset slugs) | `["intro-video", "demo-video"]` |
166
171
  | `markdown` | `string` | `"# Heading\n\nParagraph"` |
167
172
  | `jsonLd` | `object` | `{ "@context": "https://schema.org", "@type": "Article" }` |
168
173
 
174
+ ### Markdown Asset Slug Syntax
175
+
176
+ Markdown fields support direct asset slug references. Use these forms inside markdown content:
177
+
178
+ - Image embed (resolved to optimized image URL): `![Alt text](asset-slug)`
179
+ - Asset link (resolved to asset URL): `[Download file](asset-slug)`
180
+ - HTML image tag (resolved to optimized image URL): `<img src="asset-slug" width="1200" height="630" alt="Hero">`
181
+
182
+ Rules:
183
+
184
+ - Use raw slugs only (example: `hero-image`), not full URLs and not file extensions.
185
+ - Slugs must match `^[a-z0-9]+(?:-[a-z0-9]+)*$`.
186
+ - Missing slugs produce `markdown-asset` warnings (non-blocking).
187
+
169
188
  ### File Naming
170
189
 
171
190
  - Collection: `{itemSlug}.{locale}.json` (e.g., `my-post.en.json`, `my-post.fr.json`)
@@ -173,6 +173,7 @@ All Supatent files live under the `.supatent/` directory in the project root:
173
173
  - `text` -> `textInput`, `textarea`
174
174
  - `number` -> `numberInput`
175
175
  - `image` -> `singleImage`, `multiImage`
176
+ - `video` -> `singleVideo`, `multiVideo`
176
177
  - `markdown` -> `markdownEditor`
177
178
  - `jsonLd` -> `jsonLdEditor`
178
179
 
@@ -208,6 +209,19 @@ All Supatent files live under the `.supatent/` directory in the project root:
208
209
 
209
210
  **Fix:** Add the missing field to the content file with the correct value type.
210
211
 
212
+ ### Missing Markdown Asset Slug (Warning)
213
+
214
+ **Warning:** `Field 'body': markdown references missing asset slug 'hero-image'`
215
+
216
+ **Cause:** A markdown field references an asset slug that does not exist in local asset metadata. Checked forms include:
217
+ - `![alt](asset-slug)`
218
+ - `[text](asset-slug)`
219
+ - `<img src="asset-slug" ...>`
220
+
221
+ **Fix:** Create the asset metadata/file for that slug (for example `.supatent/assets/hero-image.en.json`) or update the markdown link to an existing slug.
222
+
223
+ **Agent note:** This is warning-only (`type: "markdown-asset"`). It appears in `supatent validate`, `supatent dev`, and pre-push validation, but does not block sync.
224
+
211
225
  ### JSON-LD Validation Errors
212
226
 
213
227
  **Error:** `Field 'structured-data': @root: Missing required property "@context"` or `Missing required property "@type"`
@@ -333,7 +347,7 @@ The `.validation-status.json` file is written by `supatent validate` and `supate
333
347
  **Key fields for AI agents:**
334
348
  - `valid`: `true` if no errors (warnings are acceptable)
335
349
  - `errors[]`: Each error includes `path`, `message`, and `help` (actionable fix instruction)
336
- - `warnings[]`: Non-blocking issues (locale coverage, empty JSON-LD fields)
350
+ - `warnings[]`: Non-blocking issues (locale coverage, empty JSON-LD fields, missing markdown asset slugs)
337
351
  - `trigger`: What caused this validation (`"file-change"`, `"manual"`, or `"startup"`)
338
352
 
339
353
  **Workflow for AI agents:**