nuxt-ai-ready 1.7.4 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/agent-skills.mjs +243 -0
- package/dist/chunks/prerender.mjs +497 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -2231
- package/dist/runtime/server/db/context.d.ts +1 -0
- package/dist/runtime/server/db/context.js +1 -0
- package/dist/runtime/server/db/drizzle/client.js +1 -1
- package/dist/runtime/server/middleware/markdown.js +23 -10
- package/dist/runtime/server/middleware/markdown.prerender.js +2 -1
- package/dist/runtime/server/plugins/db-lifecycle.js +5 -1
- package/dist/runtime/server/utils/markdown-request.d.ts +17 -0
- package/dist/runtime/server/utils/markdown-request.js +51 -0
- package/dist/runtime/server/utils.d.ts +1 -17
- package/dist/runtime/server/utils.js +0 -55
- package/dist/shared/nuxt-ai-ready.CDEc2X7T.mjs +1545 -0
- package/package.json +5 -5
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { realpath, readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, isAbsolute, relative, sep } from 'node:path';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.CDEc2X7T.mjs';
|
|
6
|
+
import 'node:module';
|
|
7
|
+
import '@nuxt/kit';
|
|
8
|
+
import 'defu';
|
|
9
|
+
import 'nuxt-site-config/kit';
|
|
10
|
+
import 'nuxtseo-shared/kit';
|
|
11
|
+
import 'pkg-types';
|
|
12
|
+
import 'node:url';
|
|
13
|
+
import '../../dist/runtime/server/utils/discovery-response.js';
|
|
14
|
+
import 'pathe';
|
|
15
|
+
import 'nuxtseo-shared/i18n';
|
|
16
|
+
|
|
17
|
+
const namePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
18
|
+
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
19
|
+
const urlBase = "https://example.com/.well-known/agent-skills/index.json";
|
|
20
|
+
function skillRoute(name) {
|
|
21
|
+
return `/.well-known/agent-skills/${name}/SKILL.md`;
|
|
22
|
+
}
|
|
23
|
+
function skillUrl(name) {
|
|
24
|
+
return `${name}/SKILL.md`;
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function isWithinDirectory(directory, file) {
|
|
30
|
+
const path = relative(directory, file);
|
|
31
|
+
return path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
32
|
+
}
|
|
33
|
+
function validateCommonFields(skill, index) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
if (typeof skill.name !== "string" || !namePattern.test(skill.name)) {
|
|
36
|
+
issues.push({
|
|
37
|
+
index,
|
|
38
|
+
field: "name",
|
|
39
|
+
message: "must contain 1 to 64 lowercase alphanumeric or hyphen characters, without leading, trailing, or consecutive hyphens"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (typeof skill.description !== "string" || skill.description.length === 0 || [...skill.description].length > 1024) {
|
|
43
|
+
issues.push({
|
|
44
|
+
index,
|
|
45
|
+
field: "description",
|
|
46
|
+
message: "must contain 1 to 1024 characters"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return issues;
|
|
50
|
+
}
|
|
51
|
+
function validateSkill(skill, index) {
|
|
52
|
+
if (!isRecord(skill)) {
|
|
53
|
+
return [{ index, field: "source", message: "must be a local or external skill entry" }];
|
|
54
|
+
}
|
|
55
|
+
const issues = validateCommonFields(skill, index);
|
|
56
|
+
if (skill.source === "local") {
|
|
57
|
+
if (typeof skill.file !== "string" || skill.file.trim().length === 0)
|
|
58
|
+
issues.push({ index, field: "file", message: "must be a non-empty path relative to the Nuxt root directory" });
|
|
59
|
+
return issues;
|
|
60
|
+
}
|
|
61
|
+
if (skill.source === "external") {
|
|
62
|
+
if (skill.type !== "skill-md" && skill.type !== "archive")
|
|
63
|
+
issues.push({ index, field: "type", message: 'must be "skill-md" or "archive"' });
|
|
64
|
+
if (typeof skill.url !== "string" || skill.url.trim().length === 0 || skill.url !== skill.url.trim() || !URL.canParse(skill.url, urlBase) || !["http:", "https:"].includes(new URL(skill.url, urlBase).protocol)) {
|
|
65
|
+
issues.push({ index, field: "url", message: "must be an HTTP(S), path-absolute, or relative URL" });
|
|
66
|
+
}
|
|
67
|
+
if (typeof skill.digest !== "string" || !digestPattern.test(skill.digest))
|
|
68
|
+
issues.push({ index, field: "digest", message: "must use the format sha256: followed by 64 lowercase hexadecimal characters" });
|
|
69
|
+
return issues;
|
|
70
|
+
}
|
|
71
|
+
issues.push({ index, field: "source", message: 'must be "local" or "external"' });
|
|
72
|
+
return issues;
|
|
73
|
+
}
|
|
74
|
+
function resolveExternalEntry(skill) {
|
|
75
|
+
return {
|
|
76
|
+
name: skill.name,
|
|
77
|
+
type: skill.type,
|
|
78
|
+
description: skill.description,
|
|
79
|
+
url: skill.url,
|
|
80
|
+
digest: skill.digest
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function parseLocalSkillMetadata(content, skill, index) {
|
|
84
|
+
const match = content.match(/^\uFEFF?---[\t ]*\r?\n([\s\S]*?)\r?\n---[\t ]*(?:\r?\n|$)/);
|
|
85
|
+
if (!match?.[1]) {
|
|
86
|
+
return [{
|
|
87
|
+
index,
|
|
88
|
+
field: "file",
|
|
89
|
+
message: "must contain YAML frontmatter with name and description fields"
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
const document = parseDocument(match[1], { prettyErrors: false });
|
|
93
|
+
if (document.errors.length > 0) {
|
|
94
|
+
return [{
|
|
95
|
+
index,
|
|
96
|
+
field: "file",
|
|
97
|
+
message: `contains invalid YAML frontmatter: ${document.errors[0]?.message || "unknown YAML error"}`
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
const metadata = document.toJS();
|
|
101
|
+
if (!isRecord(metadata)) {
|
|
102
|
+
return [{
|
|
103
|
+
index,
|
|
104
|
+
field: "file",
|
|
105
|
+
message: "frontmatter must be a YAML mapping with name and description fields"
|
|
106
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
const issues = [];
|
|
109
|
+
if (!content.slice(match[0].length).trim()) {
|
|
110
|
+
issues.push({
|
|
111
|
+
index,
|
|
112
|
+
field: "file",
|
|
113
|
+
message: "must contain Markdown instructions after its frontmatter"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (metadata.name !== skill.name) {
|
|
117
|
+
issues.push({
|
|
118
|
+
index,
|
|
119
|
+
field: "name",
|
|
120
|
+
message: `must exactly match the local SKILL.md frontmatter name "${String(metadata.name)}"`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (metadata.description !== skill.description) {
|
|
124
|
+
issues.push({
|
|
125
|
+
index,
|
|
126
|
+
field: "description",
|
|
127
|
+
message: "must exactly match the local SKILL.md frontmatter description"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
132
|
+
async function resolveLocalEntry(skill, index, rootDir) {
|
|
133
|
+
const file = resolve(rootDir, skill.file);
|
|
134
|
+
if (isAbsolute(skill.file) || !isWithinDirectory(rootDir, file)) {
|
|
135
|
+
return {
|
|
136
|
+
_tag: "Invalid",
|
|
137
|
+
issues: [{
|
|
138
|
+
index,
|
|
139
|
+
field: "file",
|
|
140
|
+
message: "must resolve within the Nuxt root directory"
|
|
141
|
+
}]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return Promise.all([realpath(rootDir), realpath(file)]).then(([realRootDir, realFile]) => {
|
|
145
|
+
if (!isWithinDirectory(realRootDir, realFile)) {
|
|
146
|
+
return {
|
|
147
|
+
_tag: "Invalid",
|
|
148
|
+
issues: [{
|
|
149
|
+
index,
|
|
150
|
+
field: "file",
|
|
151
|
+
message: "must resolve within the Nuxt root directory"
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return readFile(realFile).then((content) => {
|
|
156
|
+
const text = content.toString("utf8");
|
|
157
|
+
if (!Buffer.from(text, "utf8").equals(content)) {
|
|
158
|
+
return {
|
|
159
|
+
_tag: "Invalid",
|
|
160
|
+
issues: [{
|
|
161
|
+
index,
|
|
162
|
+
field: "file",
|
|
163
|
+
message: "must contain valid UTF-8 text"
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const metadataIssues = parseLocalSkillMetadata(text, skill, index);
|
|
168
|
+
if (metadataIssues.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
_tag: "Invalid",
|
|
171
|
+
issues: metadataIssues
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const route = skillRoute(skill.name);
|
|
175
|
+
const digest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
176
|
+
return {
|
|
177
|
+
_tag: "Resolved",
|
|
178
|
+
entry: {
|
|
179
|
+
name: skill.name,
|
|
180
|
+
type: "skill-md",
|
|
181
|
+
description: skill.description,
|
|
182
|
+
url: skillUrl(skill.name),
|
|
183
|
+
digest
|
|
184
|
+
},
|
|
185
|
+
route,
|
|
186
|
+
content: text
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}).catch((error) => ({
|
|
190
|
+
_tag: "Invalid",
|
|
191
|
+
issues: [{
|
|
192
|
+
index,
|
|
193
|
+
field: "file",
|
|
194
|
+
message: `could not read ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
195
|
+
}]
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
async function resolveAgentSkillsConfig(config, rootDir) {
|
|
199
|
+
if (config === false || config === void 0)
|
|
200
|
+
return { _tag: "Disabled" };
|
|
201
|
+
if (!isRecord(config) || !Array.isArray(config.skills)) {
|
|
202
|
+
return {
|
|
203
|
+
_tag: "Invalid",
|
|
204
|
+
issues: [{ field: "agentSkills", message: "must contain a skills array" }]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const issues = config.skills.flatMap((skill, index) => validateSkill(skill, index));
|
|
208
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
209
|
+
for (const [index, skill] of config.skills.entries()) {
|
|
210
|
+
if (!isRecord(skill) || typeof skill.name !== "string" || seenNames.has(skill.name)) {
|
|
211
|
+
if (isRecord(skill) && typeof skill.name === "string" && seenNames.has(skill.name))
|
|
212
|
+
issues.push({ index, field: "name", message: `duplicates the skill name "${skill.name}"` });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
seenNames.add(skill.name);
|
|
216
|
+
}
|
|
217
|
+
if (issues.length > 0)
|
|
218
|
+
return { _tag: "Invalid", issues };
|
|
219
|
+
const skills = config.skills;
|
|
220
|
+
const resolved = await Promise.all(skills.map((skill, index) => skill.source === "local" ? resolveLocalEntry(skill, index, rootDir) : Promise.resolve({ _tag: "Resolved", entry: resolveExternalEntry(skill) })));
|
|
221
|
+
const fileIssues = resolved.flatMap((result) => result._tag === "Invalid" ? result.issues : []);
|
|
222
|
+
if (fileIssues.length > 0)
|
|
223
|
+
return { _tag: "Invalid", issues: fileIssues };
|
|
224
|
+
const entries = [];
|
|
225
|
+
const localArtifacts = {};
|
|
226
|
+
for (const result of resolved) {
|
|
227
|
+
if (result._tag !== "Resolved")
|
|
228
|
+
continue;
|
|
229
|
+
entries.push(result.entry);
|
|
230
|
+
if ("route" in result)
|
|
231
|
+
localArtifacts[result.route] = result.content;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
_tag: "Enabled",
|
|
235
|
+
index: {
|
|
236
|
+
$schema: AGENT_SKILLS_SCHEMA,
|
|
237
|
+
skills: entries
|
|
238
|
+
},
|
|
239
|
+
localArtifacts
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { AGENT_SKILLS_SCHEMA, resolveAgentSkillsConfig };
|