@supatent/skills 0.1.0
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/bin/install.mjs +318 -0
- package/package.json +43 -0
- package/skills/content-blog/SKILL.md +300 -0
- package/skills/content-blog/blog-sections.md +99 -0
- package/skills/content-landing/SKILL.md +386 -0
- package/skills/content-landing/landing-sections.md +360 -0
- package/skills/core/SKILL.md +237 -0
- package/skills/references/schema-reference.md +289 -0
- package/skills/references/workflow-reference.md +345 -0
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/install.mjs - Self-contained zero-dependency installer for @supatent/skills
|
|
4
|
+
// Copies bundled skill files to .claude/skills/supatent/ with manifest tracking.
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile, mkdir, copyFile, readdir, stat } from 'node:fs/promises';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { join, dirname, relative, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
|
|
19
|
+
const TARGET_SUBDIR = join('.claude', 'skills', 'supatent');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Exported helpers (for testability)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute SHA256 hex digest of a file.
|
|
27
|
+
*/
|
|
28
|
+
export async function sha256(filePath) {
|
|
29
|
+
const content = await readFile(filePath);
|
|
30
|
+
return createHash('sha256').update(content).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Async generator that recursively yields all file paths under `dir`.
|
|
35
|
+
*/
|
|
36
|
+
export async function* walkDir(dir) {
|
|
37
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
yield* walkDir(fullPath);
|
|
42
|
+
} else {
|
|
43
|
+
yield fullPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prompt user for confirmation. Returns true if confirmed.
|
|
50
|
+
* Auto-confirms when `force` is true or stdin is not a TTY.
|
|
51
|
+
*/
|
|
52
|
+
export async function confirm(message, { force = false } = {}) {
|
|
53
|
+
if (force || !process.stdin.isTTY) return true;
|
|
54
|
+
|
|
55
|
+
const rl = createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
61
|
+
return answer.trim().toLowerCase() === 'y';
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read and parse .manifest.json. Returns null if missing or invalid.
|
|
69
|
+
*/
|
|
70
|
+
export async function readManifest(manifestPath) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (parsed && typeof parsed === 'object' && parsed.files) {
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Walk source directory and build a map of { relativePath: { sourcePath, checksum } }.
|
|
85
|
+
* Relative paths use forward slashes for cross-platform consistency.
|
|
86
|
+
*/
|
|
87
|
+
export async function buildSourceMap(sourceDir) {
|
|
88
|
+
const sourceMap = {};
|
|
89
|
+
for await (const filePath of walkDir(sourceDir)) {
|
|
90
|
+
const relPath = relative(sourceDir, filePath).split(join('a', 'b').charAt(1)).join('/');
|
|
91
|
+
const checksum = await sha256(filePath);
|
|
92
|
+
sourceMap[relPath] = { sourcePath: filePath, checksum };
|
|
93
|
+
}
|
|
94
|
+
return sourceMap;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compute diff between source files, existing manifest, and installed files.
|
|
99
|
+
* Returns { newFiles, changed, unchanged, userModified }.
|
|
100
|
+
*
|
|
101
|
+
* - newFiles: files not in manifest (fresh)
|
|
102
|
+
* - changed: source checksum differs from manifest, installed file matches manifest (safe to overwrite)
|
|
103
|
+
* - unchanged: source checksum matches manifest
|
|
104
|
+
* - userModified: installed file differs from BOTH manifest AND source (user edited it, and source also changed)
|
|
105
|
+
*/
|
|
106
|
+
export async function computeDiff(sourceMap, manifest, targetDir) {
|
|
107
|
+
const result = {
|
|
108
|
+
newFiles: [], // relative paths
|
|
109
|
+
changed: [], // relative paths
|
|
110
|
+
unchanged: [], // relative paths
|
|
111
|
+
userModified: [], // relative paths (source changed AND user edited installed copy)
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const manifestFiles = manifest ? manifest.files : {};
|
|
115
|
+
|
|
116
|
+
for (const [relPath, { checksum: sourceChecksum }] of Object.entries(sourceMap)) {
|
|
117
|
+
const manifestChecksum = manifestFiles[relPath];
|
|
118
|
+
|
|
119
|
+
if (!manifestChecksum) {
|
|
120
|
+
// Not in manifest -- new file
|
|
121
|
+
result.newFiles.push(relPath);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (sourceChecksum === manifestChecksum) {
|
|
126
|
+
// Source hasn't changed since last install
|
|
127
|
+
result.unchanged.push(relPath);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Source has changed. Check if the installed file was user-modified.
|
|
132
|
+
const installedPath = join(targetDir, relPath);
|
|
133
|
+
let installedChecksum = null;
|
|
134
|
+
try {
|
|
135
|
+
installedChecksum = await sha256(installedPath);
|
|
136
|
+
} catch {
|
|
137
|
+
// Installed file missing -- treat as changed (needs re-copy)
|
|
138
|
+
result.changed.push(relPath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (installedChecksum === manifestChecksum) {
|
|
143
|
+
// Installed file matches manifest -- user hasn't touched it, safe to overwrite
|
|
144
|
+
result.changed.push(relPath);
|
|
145
|
+
} else {
|
|
146
|
+
// Installed file differs from manifest AND source changed -- user modified
|
|
147
|
+
result.userModified.push(relPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Read version from package.json relative to bin/.
|
|
156
|
+
*/
|
|
157
|
+
export async function getVersion() {
|
|
158
|
+
const pkgPath = join(__dirname, '..', 'package.json');
|
|
159
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
160
|
+
return pkg.version;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Main installer flow
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = process.argv.slice(2);
|
|
169
|
+
const force = args.includes('--force');
|
|
170
|
+
|
|
171
|
+
const version = await getVersion();
|
|
172
|
+
const cwd = process.cwd();
|
|
173
|
+
const claudeDir = join(cwd, '.claude');
|
|
174
|
+
const targetDir = join(cwd, TARGET_SUBDIR);
|
|
175
|
+
const manifestPath = join(targetDir, '.manifest.json');
|
|
176
|
+
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
// Step a: Check .claude/ directory
|
|
179
|
+
// -----------------------------------------------------------------------
|
|
180
|
+
let claudeExists = false;
|
|
181
|
+
try {
|
|
182
|
+
const s = await stat(claudeDir);
|
|
183
|
+
claudeExists = s.isDirectory();
|
|
184
|
+
} catch {
|
|
185
|
+
// does not exist
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!claudeExists) {
|
|
189
|
+
console.log('\x1b[33m!\x1b[0m No .claude/ directory found. This installer creates skill files for Claude Code.');
|
|
190
|
+
const ok = await confirm('Create .claude/ directory and continue?', { force });
|
|
191
|
+
if (!ok) {
|
|
192
|
+
console.log('Aborted.');
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await mkdir(claudeDir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
// Step b: Walk source skills/ directory and build source map
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
const sourceMap = await buildSourceMap(SKILLS_SOURCE_DIR);
|
|
203
|
+
const sourceFiles = Object.keys(sourceMap);
|
|
204
|
+
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
// Step c: Read existing manifest
|
|
207
|
+
// -----------------------------------------------------------------------
|
|
208
|
+
const manifest = await readManifest(manifestPath);
|
|
209
|
+
|
|
210
|
+
// -----------------------------------------------------------------------
|
|
211
|
+
// Step d: Compute diff
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
const diff = await computeDiff(sourceMap, manifest, targetDir);
|
|
214
|
+
|
|
215
|
+
// -----------------------------------------------------------------------
|
|
216
|
+
// Step e: Handle user-modified files
|
|
217
|
+
// -----------------------------------------------------------------------
|
|
218
|
+
const filesToCopy = [...diff.newFiles, ...diff.changed];
|
|
219
|
+
const skippedFiles = [];
|
|
220
|
+
|
|
221
|
+
for (const relPath of diff.userModified) {
|
|
222
|
+
const ok = await confirm(`File ${relPath} has been manually modified. Overwrite?`, { force });
|
|
223
|
+
if (ok) {
|
|
224
|
+
filesToCopy.push(relPath);
|
|
225
|
+
} else {
|
|
226
|
+
skippedFiles.push(relPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
// Check if anything to do
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
if (filesToCopy.length === 0 && skippedFiles.length === 0) {
|
|
234
|
+
console.log(`\x1b[32m\u2713\x1b[0m Already up to date (v${version})`);
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// Step f: Copy files
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
const isFreshInstall = !manifest;
|
|
244
|
+
|
|
245
|
+
for (const relPath of filesToCopy) {
|
|
246
|
+
const source = sourceMap[relPath].sourcePath;
|
|
247
|
+
const target = join(targetDir, relPath);
|
|
248
|
+
await mkdir(dirname(target), { recursive: true });
|
|
249
|
+
await copyFile(source, target);
|
|
250
|
+
|
|
251
|
+
if (isFreshInstall) {
|
|
252
|
+
console.log(` \x1b[36m\u2192\x1b[0m ${relPath}`);
|
|
253
|
+
} else if (diff.newFiles.includes(relPath)) {
|
|
254
|
+
console.log(` \x1b[32m+\x1b[0m Added ${relPath}`);
|
|
255
|
+
} else {
|
|
256
|
+
console.log(` \x1b[33m\u2191\x1b[0m Updated ${relPath}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const relPath of skippedFiles) {
|
|
261
|
+
console.log(` \x1b[90m-\x1b[0m Skipped ${relPath} (user modified)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -----------------------------------------------------------------------
|
|
265
|
+
// Step g: Write manifest
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
const manifestFiles = {};
|
|
268
|
+
|
|
269
|
+
// Retain old manifest entries for skipped files
|
|
270
|
+
if (manifest && manifest.files) {
|
|
271
|
+
for (const relPath of skippedFiles) {
|
|
272
|
+
if (manifest.files[relPath]) {
|
|
273
|
+
manifestFiles[relPath] = manifest.files[relPath];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add/update entries for all source files (except skipped, which keep old entry)
|
|
279
|
+
for (const relPath of sourceFiles) {
|
|
280
|
+
if (!skippedFiles.includes(relPath)) {
|
|
281
|
+
manifestFiles[relPath] = sourceMap[relPath].checksum;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const manifestData = {
|
|
286
|
+
version,
|
|
287
|
+
installedAt: new Date().toISOString(),
|
|
288
|
+
files: manifestFiles,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
292
|
+
await writeFile(manifestPath, JSON.stringify(manifestData, null, 2) + '\n');
|
|
293
|
+
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
// Step h: Print summary
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
console.log('');
|
|
298
|
+
if (isFreshInstall) {
|
|
299
|
+
console.log(`\x1b[32m\u2713\x1b[0m Installed ${filesToCopy.length} files to ${TARGET_SUBDIR}/ (v${version})`);
|
|
300
|
+
} else {
|
|
301
|
+
console.log(`\x1b[32m\u2713\x1b[0m Updated ${filesToCopy.length} file${filesToCopy.length === 1 ? '' : 's'} in ${TARGET_SUBDIR}/ (v${version})`);
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Run main only when executed directly (not when imported for testing)
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(__filename);
|
|
312
|
+
|
|
313
|
+
if (isMain) {
|
|
314
|
+
main().catch((err) => {
|
|
315
|
+
console.error(`\x1b[31mError:\x1b[0m ${err.message}`);
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
});
|
|
318
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supatent/skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code content authoring skills for Supatent CMS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"supatent-skills": "bin/install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"lint": "eslint bin/"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"eslint": "^9.16.0",
|
|
19
|
+
"vitest": "^2.0.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.0.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"supatent",
|
|
26
|
+
"cms",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"skills",
|
|
29
|
+
"content-authoring"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": "Supatent <hello@supatent.ai>",
|
|
33
|
+
"homepage": "https://supatent.ai/docs",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/supatent/supatent.git",
|
|
37
|
+
"directory": "packages/skills"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "supatent:content-blog"
|
|
3
|
+
description: "Use when the user wants to create blog posts, update blog content, manage blog locales, fix blog SEO, or work with blog schemas (blog-post, blog-author, blog-settings)."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Supatent Blog Content Skill
|
|
9
|
+
|
|
10
|
+
You are a blog content assistant for Supatent CMS. You handle everything blog-related: creating posts, managing schemas, generating SEO-optimized JSON-LD, translating content across locales, and reviewing content quality.
|
|
11
|
+
|
|
12
|
+
You are an intelligent assistant, not a form wizard. Read context, adapt to what exists, and only ask what you do not know. Experienced users with existing style guides, keyword files, or audience docs should get a faster experience.
|
|
13
|
+
|
|
14
|
+
## Reference Files
|
|
15
|
+
|
|
16
|
+
Load these files on-demand, not upfront. Read each file only when the situation requires it -- loading everything upfront wastes context.
|
|
17
|
+
|
|
18
|
+
| File | When to Read |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| `./blog-sections.md` | Creating or modifying blog schemas -- contains full JSON definitions for blog-post, blog-author, and blog-settings |
|
|
21
|
+
| `../references/schema-reference.md` | Troubleshooting validation errors, checking field types, or looking up JSON-LD type details |
|
|
22
|
+
| `../references/workflow-reference.md` | Running CLI operations (init, dev, push, pull, validate), fixing errors, or understanding dev mode behavior |
|
|
23
|
+
|
|
24
|
+
## Intent Detection
|
|
25
|
+
|
|
26
|
+
When invoked, check the user's message to determine intent. Follow one of three paths.
|
|
27
|
+
|
|
28
|
+
### Path 1: No specific instructions
|
|
29
|
+
|
|
30
|
+
The user invoked `/supatent:content-blog` without additional text, or with a vague message like "help with my blog."
|
|
31
|
+
|
|
32
|
+
Check the current state:
|
|
33
|
+
|
|
34
|
+
1. If `.supatent/schema/blog-post.json` does not exist -- no blog is set up yet:
|
|
35
|
+
> It looks like you have not set up a blog yet. I will create the blog schemas and your first post. Let me start with a few questions.
|
|
36
|
+
Then run the Interview Flow.
|
|
37
|
+
|
|
38
|
+
2. If blog schemas exist but `.supatent/content/blog-post/` is empty or missing -- schemas exist, no content:
|
|
39
|
+
> Your blog schemas are ready but you do not have any posts yet. Want to create your first post?
|
|
40
|
+
Then run the Interview Flow.
|
|
41
|
+
|
|
42
|
+
3. If blog content already exists -- ask what the user wants:
|
|
43
|
+
> You have an active blog with [N] posts. What would you like to do?
|
|
44
|
+
> - Write a new post
|
|
45
|
+
> - Update an existing post
|
|
46
|
+
> - Translate posts to another locale
|
|
47
|
+
> - Review SEO and structured data
|
|
48
|
+
> - Something else
|
|
49
|
+
|
|
50
|
+
### Path 2: Specific instructions
|
|
51
|
+
|
|
52
|
+
The user gave a clear directive (e.g., "translate posts to French", "update SEO on my posts", "I added a new locale").
|
|
53
|
+
|
|
54
|
+
Do NOT run the Interview Flow. Instead:
|
|
55
|
+
|
|
56
|
+
1. Investigate current state: read existing schemas, content files, and locale patterns
|
|
57
|
+
2. Act on the request directly, asking clarifying questions only as needed
|
|
58
|
+
3. For destructive operations (editing existing files): show what will change and ask for confirmation before writing
|
|
59
|
+
|
|
60
|
+
### Path 3: New post request
|
|
61
|
+
|
|
62
|
+
The user wants a new post (e.g., "write a post about X", "create a blog post about AI tools").
|
|
63
|
+
|
|
64
|
+
Run the Interview Flow, but pre-fill answers from the invocation message. Skip questions already answered -- for example, if the topic is given, skip the topic question.
|
|
65
|
+
|
|
66
|
+
### Intent signals
|
|
67
|
+
|
|
68
|
+
| Signal words | Intent |
|
|
69
|
+
|-------------|--------|
|
|
70
|
+
| "new post", "write", "create", or no text | New post creation |
|
|
71
|
+
| "translate", "locale", "language" | Multi-locale management |
|
|
72
|
+
| "SEO", "JSON-LD", "structured data" | JSON-LD review and fix |
|
|
73
|
+
| "update", "edit", "change" | Content modification |
|
|
74
|
+
| "check", "review", "quality" | Content quality review |
|
|
75
|
+
|
|
76
|
+
## Context Discovery
|
|
77
|
+
|
|
78
|
+
Before the interview, gather existing context that can pre-fill answers. This reduces questions the user must answer.
|
|
79
|
+
|
|
80
|
+
### Project context files
|
|
81
|
+
|
|
82
|
+
Scan the project root and common documentation directories for:
|
|
83
|
+
|
|
84
|
+
- **Tone/style:** files matching `*tone*guide*`, `*style*guide*`
|
|
85
|
+
- **SEO keywords:** files matching `*keyword*`, `*seo*`
|
|
86
|
+
- **Audience:** files matching `*audience*`, `*persona*`
|
|
87
|
+
|
|
88
|
+
If the user mentions a specific document, read it directly.
|
|
89
|
+
|
|
90
|
+
### Locale configuration
|
|
91
|
+
|
|
92
|
+
Discover what locales the project uses by scanning existing content files.
|
|
93
|
+
|
|
94
|
+
Scan `.supatent/content/` for files matching `*.{locale}.json`. Extract unique locale codes to identify:
|
|
95
|
+
|
|
96
|
+
- **Primary locale:** the locale with the most content files
|
|
97
|
+
- **Secondary locales:** all other detected locale codes
|
|
98
|
+
|
|
99
|
+
If no content files exist yet, assume `en` as the primary locale and ask the user to confirm.
|
|
100
|
+
|
|
101
|
+
## Interview Flow
|
|
102
|
+
|
|
103
|
+
The full interview runs ONLY for new blog post creation. It has five questions. Skip any question where context already provides the answer.
|
|
104
|
+
|
|
105
|
+
**Question 1 -- Topic and angle** (free-form)
|
|
106
|
+
"What topic do you want to write about? What is your angle or thesis?"
|
|
107
|
+
Skip if: the user stated the topic in their invocation message.
|
|
108
|
+
|
|
109
|
+
**Question 2 -- Target audience** (free-form with suggestions)
|
|
110
|
+
"Who is this post for? Examples: developers, marketers, executives, general audience."
|
|
111
|
+
Skip if: an audience or persona document was found in the project, or the user mentioned the audience.
|
|
112
|
+
|
|
113
|
+
**Question 3 -- Key points** (free-form)
|
|
114
|
+
"What are the 3-5 main points or takeaways you want to cover?"
|
|
115
|
+
Skip if: the user provided a detailed outline in their invocation message.
|
|
116
|
+
|
|
117
|
+
**Question 4 -- Tone and style** (multi-choice)
|
|
118
|
+
"What tone works best?
|
|
119
|
+
(a) Professional and authoritative
|
|
120
|
+
(b) Conversational and friendly
|
|
121
|
+
(c) Technical and detailed
|
|
122
|
+
(d) Casual and approachable"
|
|
123
|
+
Skip if: a tone or style guide file was found in the project.
|
|
124
|
+
|
|
125
|
+
**Question 5 -- SEO keywords** (free-form)
|
|
126
|
+
"What keywords should this post target? For example: 'content strategy', 'SEO best practices'."
|
|
127
|
+
Skip if: an SEO keyword list was found in the project.
|
|
128
|
+
|
|
129
|
+
Ask all unanswered questions in a single message. Group them naturally as a conversation, not a numbered form.
|
|
130
|
+
|
|
131
|
+
After gathering answers, summarize the brief back to the user:
|
|
132
|
+
> Here is what I will create: [topic summary, audience, tone, key points]. Ready to generate?
|
|
133
|
+
|
|
134
|
+
Then proceed to schema generation (if needed) and content generation.
|
|
135
|
+
|
|
136
|
+
## Schema Generation
|
|
137
|
+
|
|
138
|
+
Read `./blog-sections.md` to get the schema definitions.
|
|
139
|
+
|
|
140
|
+
### First-time setup (no blog schemas exist)
|
|
141
|
+
|
|
142
|
+
1. Create `.supatent/schema/blog-post.json` with the core fields from the catalog (title, excerpt, cover-image, body, json-ld)
|
|
143
|
+
2. Ask the user about optional fields:
|
|
144
|
+
> The blog-post schema has three optional fields. Want me to add any of these?
|
|
145
|
+
> - **date-published** -- publication date (YYYY-MM-DD format)
|
|
146
|
+
> - **author** -- links to a blog-author entry by slug
|
|
147
|
+
> - **category** -- post category for filtering
|
|
148
|
+
Add the fields the user selects, adjusting `order` values accordingly.
|
|
149
|
+
3. Create `.supatent/schema/blog-author.json`
|
|
150
|
+
4. Create `.supatent/schema/blog-settings.json`
|
|
151
|
+
5. Ask the user for their blog title, then create `.supatent/content/blog-settings/default.{primaryLocale}.json` with:
|
|
152
|
+
- `blog-title`: the user's answer
|
|
153
|
+
- `blog-description`: ask or generate a sensible default
|
|
154
|
+
- `posts-per-page`: default to 10
|
|
155
|
+
|
|
156
|
+
If schemas already exist, skip this section entirely.
|
|
157
|
+
|
|
158
|
+
After writing schema files, check `.supatent/.validation-status.json` to confirm validation passes. If errors appear, read `../references/workflow-reference.md` for fix instructions.
|
|
159
|
+
|
|
160
|
+
## Content Generation
|
|
161
|
+
|
|
162
|
+
Generate content based on interview answers. This is the core creative work.
|
|
163
|
+
|
|
164
|
+
### Blog post
|
|
165
|
+
|
|
166
|
+
Write to `.supatent/content/blog-post/{post-slug}.{locale}.json`.
|
|
167
|
+
|
|
168
|
+
Generate a URL-friendly slug from the title (lowercase, hyphens, no special characters).
|
|
169
|
+
|
|
170
|
+
**Body content:**
|
|
171
|
+
- Default length: 1000-1500 words
|
|
172
|
+
- Format: markdown with headings (H2, H3), paragraphs, lists, and emphasis as the content demands
|
|
173
|
+
- SEO-conscious writing: include target keywords in the first paragraph, use semantic keyword variations throughout, structure headings around searchable concepts
|
|
174
|
+
- 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
|
+
- Match the tone and style the user selected in the interview
|
|
176
|
+
|
|
177
|
+
**Field values:**
|
|
178
|
+
|
|
179
|
+
| Field | Value |
|
|
180
|
+
|-------|-------|
|
|
181
|
+
| `title` | Clear, engaging title incorporating the primary keyword |
|
|
182
|
+
| `excerpt` | 1-2 sentence summary, 150-160 characters ideal for meta description |
|
|
183
|
+
| `cover-image` | Empty string `""` -- note to user: "Add a cover image asset slug after uploading via the dashboard or CLI" |
|
|
184
|
+
| `body` | The full markdown article |
|
|
185
|
+
| `date-published` | Today's date in YYYY-MM-DD format (if field exists on schema) |
|
|
186
|
+
| `author` | Empty string `""` unless a blog-author content item exists (if field exists) |
|
|
187
|
+
| `category` | Derived from topic (if field exists on schema) |
|
|
188
|
+
| `json-ld` | Article structured data -- see JSON-LD section below |
|
|
189
|
+
|
|
190
|
+
### Blog author (first-time setup only)
|
|
191
|
+
|
|
192
|
+
If this is the first blog setup:
|
|
193
|
+
1. Ask the user for their name
|
|
194
|
+
2. Write to `.supatent/content/blog-author/{name-slug}.{locale}.json` with the `name` field
|
|
195
|
+
3. Set the `author` field on the blog post to this author's slug
|
|
196
|
+
|
|
197
|
+
### Content writing skill delegation
|
|
198
|
+
|
|
199
|
+
If the user has a content writing skill installed (check `.claude/skills/` for writing-related skills), defer to that skill for the actual body content and handle only the Supatent-specific parts: schema creation, file structure, JSON-LD generation, and validation.
|
|
200
|
+
|
|
201
|
+
## JSON-LD Article Generation
|
|
202
|
+
|
|
203
|
+
Generate the `json-ld` field value for each blog post. Follow these rules strictly.
|
|
204
|
+
|
|
205
|
+
### Required rules
|
|
206
|
+
|
|
207
|
+
1. Use `@type: "Article"` -- NEVER `BlogPosting`. BlogPosting fails Supatent validation.
|
|
208
|
+
2. Always include `@context: "https://schema.org"`.
|
|
209
|
+
3. Include these properties when data is available:
|
|
210
|
+
- `headline`: same as the post title (max 110 characters)
|
|
211
|
+
- `author`: `{ "@type": "Person", "name": "Author Name" }` -- `name` is REQUIRED on the Person object
|
|
212
|
+
- `datePublished`: same as date-published field value (YYYY-MM-DD)
|
|
213
|
+
- `description`: same as excerpt
|
|
214
|
+
- `wordCount`: calculated from the body content
|
|
215
|
+
- `articleSection`: same as category if available
|
|
216
|
+
4. Do NOT include `image` unless the user provides an actual URL. Asset slugs fail URI validation.
|
|
217
|
+
5. Do NOT include `dateModified` on initial creation.
|
|
218
|
+
6. Do NOT include properties not in the Article schema. The validator uses `additionalProperties: false`.
|
|
219
|
+
|
|
220
|
+
### Example
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"@context": "https://schema.org",
|
|
225
|
+
"@type": "Article",
|
|
226
|
+
"headline": "Post Title Here",
|
|
227
|
+
"author": {
|
|
228
|
+
"@type": "Person",
|
|
229
|
+
"name": "Author Name"
|
|
230
|
+
},
|
|
231
|
+
"datePublished": "2026-02-13",
|
|
232
|
+
"description": "Brief description of the post.",
|
|
233
|
+
"wordCount": 1250,
|
|
234
|
+
"articleSection": "Category"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Troubleshooting
|
|
239
|
+
|
|
240
|
+
After writing, check `.supatent/.validation-status.json` for errors. Common JSON-LD issues:
|
|
241
|
+
|
|
242
|
+
| Error | Cause | Fix |
|
|
243
|
+
|-------|-------|-----|
|
|
244
|
+
| `unsupported @type` | Using BlogPosting instead of Article | Change `@type` to `"Article"` |
|
|
245
|
+
| `image: expected uri format` | Using asset slug instead of URL | Remove `image` or use a full URL |
|
|
246
|
+
| `required property 'name' is missing` | Author object missing name | Add `"name"` to the author Person object |
|
|
247
|
+
|
|
248
|
+
## Multi-Locale Translation
|
|
249
|
+
|
|
250
|
+
After generating the primary locale content, handle translations for other configured locales.
|
|
251
|
+
|
|
252
|
+
1. Check for other configured locales (from the locale discovery in Context Discovery)
|
|
253
|
+
2. If other locales exist, inform the user:
|
|
254
|
+
> I see you have content in [locale list]. I will translate the post to these locales.
|
|
255
|
+
3. Generate translated content files: `.supatent/content/blog-post/{slug}.{locale}.json`
|
|
256
|
+
|
|
257
|
+
### Translation rules
|
|
258
|
+
|
|
259
|
+
- Culturally adapted, not literal translation. Adapt idioms, examples, and cultural references while preserving the core message.
|
|
260
|
+
- Keep the same slug across all locales.
|
|
261
|
+
- Translate all text fields: title, excerpt, body.
|
|
262
|
+
- JSON-LD: translate headline and description. Keep author name and datePublished unchanged.
|
|
263
|
+
- Do NOT translate field keys -- they are schema slugs, not content.
|
|
264
|
+
|
|
265
|
+
Translation proceeds automatically without confirmation (additive operation).
|
|
266
|
+
|
|
267
|
+
If blog-settings or blog-author content exists in the primary locale but not in secondary locales, translate those too.
|
|
268
|
+
|
|
269
|
+
If only one locale is configured, skip this section entirely.
|
|
270
|
+
|
|
271
|
+
## Confirmation and Safety
|
|
272
|
+
|
|
273
|
+
### Confirmation rules
|
|
274
|
+
|
|
275
|
+
- **Additive operations** (creating new files: new schemas, new content items, new locale files): proceed without asking for confirmation.
|
|
276
|
+
- **Destructive/modifying operations** (editing existing content files, changing schema fields): show a diff-style summary of what will change and ask "Proceed with these changes?" before writing.
|
|
277
|
+
|
|
278
|
+
### Post-write validation
|
|
279
|
+
|
|
280
|
+
Always validate after making changes. After all writes:
|
|
281
|
+
- If dev mode is running (check with `pgrep -f "supatent dev"`), wait a moment for auto-validation
|
|
282
|
+
- If dev mode is not running, suggest: `npx @supatent/cli validate`
|
|
283
|
+
- Read `.supatent/.validation-status.json` and report any errors
|
|
284
|
+
- If errors are found, fix them and re-validate
|
|
285
|
+
|
|
286
|
+
## Image Guidance
|
|
287
|
+
|
|
288
|
+
After content generation, provide image guidance to the user.
|
|
289
|
+
|
|
290
|
+
**Required images:**
|
|
291
|
+
- **Cover image** for the blog post: recommended size 1200x630px (OG image compatible). Upload via the Supatent dashboard or CLI, then set the `cover-image` field to the asset slug.
|
|
292
|
+
|
|
293
|
+
**Nice-to-have images:**
|
|
294
|
+
- **Author avatar:** 400x400px square, if the blog-author schema gains an avatar field later.
|
|
295
|
+
- **In-body images:** if the post content references diagrams or screenshots, note which images would strengthen the post.
|
|
296
|
+
|
|
297
|
+
Format the guidance as:
|
|
298
|
+
> Here are the images you will need for this post: [list with recommended sizes and format notes]
|
|
299
|
+
|
|
300
|
+
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.
|