toolcraft 0.0.38 → 0.0.40
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 +41 -4
- package/dist/cli.js +40 -9
- package/dist/http-errors.d.ts +57 -0
- package/dist/http-errors.js +72 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +15 -0
- package/dist/mcp-proxy.js +16 -1
- package/dist/mcp.js +224 -12
- package/node_modules/@poe-code/frontmatter/README.md +35 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
- package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
- package/node_modules/@poe-code/frontmatter/package.json +25 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +25 -1
- package/node_modules/tiny-mcp-client/README.md +7 -0
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +3 -0
- package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +10 -0
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +2 -8
- package/node_modules/tiny-mcp-client/src/internal.ts +6 -0
- package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +10 -0
- package/node_modules/toolcraft-design/README.md +12 -0
- package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
- package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
- package/node_modules/toolcraft-design/package.json +6 -3
- package/package.json +9 -6
package/dist/mcp.js
CHANGED
|
@@ -128,14 +128,24 @@ function formatAvailableList(values) {
|
|
|
128
128
|
return `Available: ${[...values].sort().join(", ")}.`;
|
|
129
129
|
}
|
|
130
130
|
function applySchemaCasing(schema, casing) {
|
|
131
|
+
const oneOf = schema.oneOf?.map((child) => applySchemaCasing(child, casing));
|
|
132
|
+
const additionalProperties = typeof schema.additionalProperties === "object" && schema.additionalProperties !== null
|
|
133
|
+
? applySchemaCasing(schema.additionalProperties, casing)
|
|
134
|
+
: schema.additionalProperties;
|
|
131
135
|
if (schema.type !== "object" || schema.properties === undefined) {
|
|
132
136
|
if (schema.type === "array" && schema.items !== undefined) {
|
|
133
137
|
return {
|
|
134
138
|
...schema,
|
|
135
|
-
|
|
139
|
+
...(additionalProperties === undefined ? {} : { additionalProperties }),
|
|
140
|
+
items: applySchemaCasing(schema.items, casing),
|
|
141
|
+
...(oneOf === undefined ? {} : { oneOf })
|
|
136
142
|
};
|
|
137
143
|
}
|
|
138
|
-
return
|
|
144
|
+
return {
|
|
145
|
+
...schema,
|
|
146
|
+
...(additionalProperties === undefined ? {} : { additionalProperties }),
|
|
147
|
+
...(oneOf === undefined ? {} : { oneOf })
|
|
148
|
+
};
|
|
139
149
|
}
|
|
140
150
|
const properties = Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [
|
|
141
151
|
formatSegment(key, casing),
|
|
@@ -144,8 +154,10 @@ function applySchemaCasing(schema, casing) {
|
|
|
144
154
|
const required = schema.required?.map((key) => formatSegment(key, casing));
|
|
145
155
|
return {
|
|
146
156
|
...schema,
|
|
157
|
+
...(additionalProperties === undefined ? {} : { additionalProperties }),
|
|
147
158
|
properties,
|
|
148
|
-
...(required === undefined ? {} : { required })
|
|
159
|
+
...(required === undefined ? {} : { required }),
|
|
160
|
+
...(oneOf === undefined ? {} : { oneOf })
|
|
149
161
|
};
|
|
150
162
|
}
|
|
151
163
|
function collectParamSummaries(schema, casing, path = [], inheritedOptional = false) {
|
|
@@ -162,16 +174,32 @@ function collectParamSummaries(schema, casing, path = [], inheritedOptional = fa
|
|
|
162
174
|
}
|
|
163
175
|
return summaries;
|
|
164
176
|
}
|
|
165
|
-
function buildToolDescription(description, params, casing) {
|
|
177
|
+
function buildToolDescription(description, params, examples, commandName, casing) {
|
|
166
178
|
const summary = collectParamSummaries(params, casing);
|
|
167
179
|
const parameterSummary = summary.length === 0 ? "" : `Parameters: ${summary.join(", ")}.`;
|
|
180
|
+
const exampleSummary = examples.length === 0
|
|
181
|
+
? ""
|
|
182
|
+
: `\n\nExamples:\n${examples
|
|
183
|
+
.map((example) => `- ${example.title}: ${commandName} ${formatMcpExampleParams(example.params)}`)
|
|
184
|
+
.join("\n")}`;
|
|
168
185
|
if (description === undefined) {
|
|
169
|
-
return parameterSummary
|
|
186
|
+
return `${parameterSummary}${exampleSummary}`;
|
|
170
187
|
}
|
|
171
188
|
if (parameterSummary.length === 0) {
|
|
172
|
-
return description
|
|
189
|
+
return `${description}${exampleSummary}`;
|
|
173
190
|
}
|
|
174
|
-
return `${description} ${parameterSummary}`;
|
|
191
|
+
return `${description} ${parameterSummary}${exampleSummary}`;
|
|
192
|
+
}
|
|
193
|
+
function formatMcpExampleParams(params) {
|
|
194
|
+
return Object.entries(params)
|
|
195
|
+
.map(([key, value]) => `${key}=${formatMcpExampleValue(value)}`)
|
|
196
|
+
.join(" ");
|
|
197
|
+
}
|
|
198
|
+
function formatMcpExampleValue(value) {
|
|
199
|
+
if (typeof value === "string") {
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
return JSON.stringify(value);
|
|
175
203
|
}
|
|
176
204
|
function matchesAllowlist(toolName, allowlist) {
|
|
177
205
|
if (allowlist === undefined) {
|
|
@@ -216,6 +244,9 @@ function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
|
|
|
216
244
|
throw new ToolcraftBugError(`command "${name}" must define an object params schema for MCP.`);
|
|
217
245
|
}
|
|
218
246
|
validateUniqueMCPParameterFields(params, casing);
|
|
247
|
+
if (node.result !== undefined) {
|
|
248
|
+
validateUniqueMCPParameterFields(node.result, casing);
|
|
249
|
+
}
|
|
219
250
|
const resolvedCommandPath = [...commandPath, node.name].join(".");
|
|
220
251
|
const existingPath = commandPathsByToolName.get(name);
|
|
221
252
|
if (existingPath !== undefined) {
|
|
@@ -226,8 +257,14 @@ function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
|
|
|
226
257
|
command: node,
|
|
227
258
|
commandPath: resolvedCommandPath,
|
|
228
259
|
name,
|
|
229
|
-
description: buildToolDescription(node.description, params, casing),
|
|
230
|
-
inputSchema: applySchemaCasing(toJsonSchema(params), casing)
|
|
260
|
+
description: buildToolDescription(node.description, params, node.examples, node.name, casing),
|
|
261
|
+
inputSchema: applySchemaCasing(toJsonSchema(params), casing),
|
|
262
|
+
...(node.result === undefined
|
|
263
|
+
? {}
|
|
264
|
+
: {
|
|
265
|
+
outputSchema: applySchemaCasing(toJsonSchema(node.result), casing),
|
|
266
|
+
resultSchema: node.result
|
|
267
|
+
})
|
|
231
268
|
});
|
|
232
269
|
return;
|
|
233
270
|
}
|
|
@@ -466,6 +503,166 @@ function validateToolArguments(schema, argumentsValue, casing) {
|
|
|
466
503
|
throwValidationErrors(errors);
|
|
467
504
|
return result;
|
|
468
505
|
}
|
|
506
|
+
function serializeResultValue(schema, value, casing, label, errors) {
|
|
507
|
+
const unwrappedSchema = unwrapOptional(schema);
|
|
508
|
+
if (value === null && unwrappedSchema.nullable === true) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
switch (unwrappedSchema.kind) {
|
|
512
|
+
case "object":
|
|
513
|
+
return serializeResultObject(unwrappedSchema, value, casing, label, errors);
|
|
514
|
+
case "array":
|
|
515
|
+
if (!Array.isArray(value)) {
|
|
516
|
+
errors.push({
|
|
517
|
+
path: label,
|
|
518
|
+
message: `Invalid value for "${label}". Expected an array, got ${describeReceived(value)}.`
|
|
519
|
+
});
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
return value.map((item, index) => serializeResultValue(unwrappedSchema.item, item, casing, `${label}[${index}]`, errors));
|
|
523
|
+
case "record":
|
|
524
|
+
if (!isPlainObject(value)) {
|
|
525
|
+
errors.push({
|
|
526
|
+
path: label,
|
|
527
|
+
message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
|
|
528
|
+
});
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
532
|
+
key,
|
|
533
|
+
serializeResultValue(unwrappedSchema.value, item, casing, `${label}.${key}`, errors)
|
|
534
|
+
]));
|
|
535
|
+
case "oneOf": {
|
|
536
|
+
if (!isPlainObject(value)) {
|
|
537
|
+
errors.push({
|
|
538
|
+
path: label,
|
|
539
|
+
message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
|
|
540
|
+
});
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
const discriminator = value[unwrappedSchema.discriminator];
|
|
544
|
+
const branch = typeof discriminator === "string" ? unwrappedSchema.branches[discriminator] : undefined;
|
|
545
|
+
if (branch === undefined) {
|
|
546
|
+
const branchNames = Object.keys(unwrappedSchema.branches);
|
|
547
|
+
errors.push({
|
|
548
|
+
path: label.length === 0 ? unwrappedSchema.discriminator : `${label}.${unwrappedSchema.discriminator}`,
|
|
549
|
+
message: `Invalid value for "${label.length === 0 ? unwrappedSchema.discriminator : `${label}.${unwrappedSchema.discriminator}`}". Expected one of: ${branchNames.join(", ")}, got ${describeReceived(discriminator)}.`
|
|
550
|
+
});
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
const { [unwrappedSchema.discriminator]: ignoredDiscriminator, ...branchValue } = value;
|
|
554
|
+
void ignoredDiscriminator;
|
|
555
|
+
return {
|
|
556
|
+
[formatSegment(unwrappedSchema.discriminator, casing)]: discriminator,
|
|
557
|
+
...serializeResultObject(branch, branchValue, casing, label, errors)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
case "union": {
|
|
561
|
+
if (!isPlainObject(value)) {
|
|
562
|
+
errors.push({
|
|
563
|
+
path: label,
|
|
564
|
+
message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
|
|
565
|
+
});
|
|
566
|
+
return value;
|
|
567
|
+
}
|
|
568
|
+
const branch = unwrappedSchema.branches.find((candidate) => Object.keys(candidate.shape).every((key) => candidate.shape[key]?.kind === "optional" ||
|
|
569
|
+
Object.prototype.hasOwnProperty.call(value, key)));
|
|
570
|
+
if (branch === undefined) {
|
|
571
|
+
errors.push({
|
|
572
|
+
path: label,
|
|
573
|
+
message: `Invalid value for "${label}". Expected one union branch, got ${describeReceived(value)}.`
|
|
574
|
+
});
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
return serializeResultObject(branch, value, casing, label, errors);
|
|
578
|
+
}
|
|
579
|
+
default:
|
|
580
|
+
return validateSchemaValue(schema, value, casing, label, errors);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function serializeResultObject(schema, value, casing, label, errors) {
|
|
584
|
+
if (!isPlainObject(value)) {
|
|
585
|
+
errors.push({
|
|
586
|
+
path: label,
|
|
587
|
+
message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
|
|
588
|
+
});
|
|
589
|
+
return {};
|
|
590
|
+
}
|
|
591
|
+
const result = {};
|
|
592
|
+
const expectedKeys = new Set(Object.keys(schema.shape));
|
|
593
|
+
for (const key of Object.keys(value)) {
|
|
594
|
+
if (!expectedKeys.has(key) && schema.additionalProperties !== true) {
|
|
595
|
+
const fieldLabel = label.length === 0 ? key : `${label}.${key}`;
|
|
596
|
+
errors.push({
|
|
597
|
+
path: fieldLabel,
|
|
598
|
+
message: `Unexpected result field "${fieldLabel}". ${formatAvailableList([...expectedKeys].map((expectedKey) => label.length === 0 ? expectedKey : `${label}.${expectedKey}`))}`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
for (const [sourceKey, rawChildSchema] of Object.entries(schema.shape)) {
|
|
603
|
+
const childSchema = unwrapOptional(rawChildSchema);
|
|
604
|
+
const hasValue = Object.prototype.hasOwnProperty.call(value, sourceKey);
|
|
605
|
+
const wireKey = formatSegment(sourceKey, casing);
|
|
606
|
+
const fieldLabel = label.length === 0 ? sourceKey : `${label}.${sourceKey}`;
|
|
607
|
+
if (!hasValue) {
|
|
608
|
+
if (childSchema.default !== undefined) {
|
|
609
|
+
Object.defineProperty(result, wireKey, {
|
|
610
|
+
value: childSchema.default,
|
|
611
|
+
enumerable: true,
|
|
612
|
+
configurable: true,
|
|
613
|
+
writable: true
|
|
614
|
+
});
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (isOptional(rawChildSchema)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
errors.push({ path: fieldLabel, message: `Missing required result field "${fieldLabel}".` });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
Object.defineProperty(result, wireKey, {
|
|
624
|
+
value: serializeResultValue(rawChildSchema, value[sourceKey], casing, fieldLabel, errors),
|
|
625
|
+
enumerable: true,
|
|
626
|
+
configurable: true,
|
|
627
|
+
writable: true
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (schema.additionalProperties === true) {
|
|
631
|
+
for (const key of Object.keys(value)) {
|
|
632
|
+
if (!expectedKeys.has(key)) {
|
|
633
|
+
Object.defineProperty(result, key, {
|
|
634
|
+
value: value[key],
|
|
635
|
+
enumerable: true,
|
|
636
|
+
configurable: true,
|
|
637
|
+
writable: true
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
function validateCommandResult(schema, value, casing) {
|
|
645
|
+
const errors = [];
|
|
646
|
+
const result = serializeResultObject(schema, value, casing, "", errors);
|
|
647
|
+
throwResultValidationErrors(errors);
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function throwResultValidationErrors(errors) {
|
|
651
|
+
if (errors.length === 0) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (errors.length === 1) {
|
|
655
|
+
throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, errors[0]?.message ?? "Invalid command result.");
|
|
656
|
+
}
|
|
657
|
+
const rendered = errors
|
|
658
|
+
.slice(0, 10)
|
|
659
|
+
.map((error) => ` - ${error.path}: ${error.message}`);
|
|
660
|
+
const remaining = errors.length - rendered.length;
|
|
661
|
+
if (remaining > 0) {
|
|
662
|
+
rendered.push(` ... and ${remaining} more`);
|
|
663
|
+
}
|
|
664
|
+
throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `${errors.length} result errors:\n${rendered.join("\n")}`);
|
|
665
|
+
}
|
|
469
666
|
function isContentBlock(value) {
|
|
470
667
|
if (!isPlainObject(value) || typeof value.type !== "string") {
|
|
471
668
|
return false;
|
|
@@ -491,7 +688,9 @@ function toToolContent(result) {
|
|
|
491
688
|
if (isContentBlock(result)) {
|
|
492
689
|
return [result];
|
|
493
690
|
}
|
|
494
|
-
|
|
691
|
+
const fallbackValue = result;
|
|
692
|
+
const fallbackText = JSON.stringify(fallbackValue);
|
|
693
|
+
return [{ type: "text", text: fallbackText }];
|
|
495
694
|
}
|
|
496
695
|
function toToolError(error) {
|
|
497
696
|
if (error instanceof ToolError) {
|
|
@@ -524,7 +723,7 @@ function createResolvedMCPServer(root, options) {
|
|
|
524
723
|
validateToolArguments: false
|
|
525
724
|
});
|
|
526
725
|
for (const tool of tools) {
|
|
527
|
-
|
|
726
|
+
const handler = async (argumentsValue) => {
|
|
528
727
|
let params;
|
|
529
728
|
let secrets;
|
|
530
729
|
try {
|
|
@@ -548,6 +747,13 @@ function createResolvedMCPServer(root, options) {
|
|
|
548
747
|
if (isHumanInLoopPending(result)) {
|
|
549
748
|
return renderPendingApproval(result);
|
|
550
749
|
}
|
|
750
|
+
if (tool.resultSchema !== undefined) {
|
|
751
|
+
const structuredContent = validateCommandResult(tool.resultSchema, result, casing);
|
|
752
|
+
return {
|
|
753
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
754
|
+
structuredContent
|
|
755
|
+
};
|
|
756
|
+
}
|
|
551
757
|
return toToolContent(result);
|
|
552
758
|
}
|
|
553
759
|
catch (error) {
|
|
@@ -566,7 +772,13 @@ function createResolvedMCPServer(root, options) {
|
|
|
566
772
|
});
|
|
567
773
|
throw toToolError(error);
|
|
568
774
|
}
|
|
569
|
-
}
|
|
775
|
+
};
|
|
776
|
+
if (tool.outputSchema === undefined) {
|
|
777
|
+
server.tool(tool.name, tool.description, tool.inputSchema, handler);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
server.tool(tool.name, tool.description, tool.inputSchema, handler, tool.outputSchema);
|
|
781
|
+
}
|
|
570
782
|
}
|
|
571
783
|
return {
|
|
572
784
|
...server,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @poe-code/frontmatter
|
|
2
|
+
|
|
3
|
+
Shared YAML frontmatter parsing for poe-code packages.
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
FrontmatterParseError,
|
|
10
|
+
parseFrontmatter,
|
|
11
|
+
parseFrontmatterDocument,
|
|
12
|
+
stringifyFrontmatter
|
|
13
|
+
} from "@poe-code/frontmatter";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `parseFrontmatter(source)` returns `{ frontmatter, body }`.
|
|
17
|
+
- `parseFrontmatterDocument(source)` returns `{ frontmatter, body, errors, lineCounter }` for callers that need YAML diagnostics.
|
|
18
|
+
- `stringifyFrontmatter(frontmatter, body)` writes `---` fences, YAML, and the body.
|
|
19
|
+
- `FrontmatterParseError` is thrown for malformed frontmatter, invalid YAML, non-object frontmatter, and stringify failures.
|
|
20
|
+
|
|
21
|
+
When no leading frontmatter block exists, parsing returns `{ frontmatter: {}, body: source }`.
|
|
22
|
+
The returned `body` is sliced from the original input and is otherwise byte-for-byte unchanged.
|
|
23
|
+
YAML mappings that contain `__proto__` keep it as an own property without changing the object's prototype.
|
|
24
|
+
|
|
25
|
+
Duplicate mapping keys use YAML's last-wins behavior by default, matching the
|
|
26
|
+
legacy frontmatter readers. Callers that need strict YAML mappings can pass
|
|
27
|
+
`{ uniqueKeys: true }` to `parseFrontmatter` or `parseFrontmatterDocument`.
|
|
28
|
+
|
|
29
|
+
## Environment
|
|
30
|
+
|
|
31
|
+
This package reads no environment variables.
|
|
32
|
+
|
|
33
|
+
## Config
|
|
34
|
+
|
|
35
|
+
This package has no configuration options.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function splitFrontmatterBlock(source) {
|
|
2
|
+
const content = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
3
|
+
const opening = readOpeningFence(content);
|
|
4
|
+
if (opening === undefined) {
|
|
5
|
+
return { body: source };
|
|
6
|
+
}
|
|
7
|
+
const closing = findClosingFence(content, opening.next);
|
|
8
|
+
if (closing === undefined) {
|
|
9
|
+
throw new Error("Missing YAML frontmatter end delimiter (---).");
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
raw: content.slice(opening.next, closing.index),
|
|
13
|
+
body: content.slice(closing.end + closing.lineBreakLength)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function readOpeningFence(source) {
|
|
17
|
+
if (!source.startsWith("---")) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const lineEnd = findLineEnd(source, 0);
|
|
21
|
+
if (lineEnd.lineBreakLength === 0 || source.slice(0, lineEnd.index) !== "---") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return { next: lineEnd.index + lineEnd.lineBreakLength };
|
|
25
|
+
}
|
|
26
|
+
function findClosingFence(source, start) {
|
|
27
|
+
let lineStart = start;
|
|
28
|
+
while (lineStart <= source.length) {
|
|
29
|
+
const lineEnd = findLineEnd(source, lineStart);
|
|
30
|
+
if (isClosingFenceLine(source.slice(lineStart, lineEnd.index))) {
|
|
31
|
+
return {
|
|
32
|
+
index: lineStart,
|
|
33
|
+
end: lineEnd.index,
|
|
34
|
+
lineBreakLength: lineEnd.lineBreakLength
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (lineEnd.lineBreakLength === 0) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
lineStart = lineEnd.index + lineEnd.lineBreakLength;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function isClosingFenceLine(line) {
|
|
45
|
+
if (!line.startsWith("---")) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
for (let index = 3; index < line.length; index += 1) {
|
|
49
|
+
const character = line[index];
|
|
50
|
+
if (character !== " " && character !== "\t") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function findLineEnd(source, start) {
|
|
57
|
+
let index = start;
|
|
58
|
+
while (index < source.length) {
|
|
59
|
+
const character = source[index];
|
|
60
|
+
if (character === "\n") {
|
|
61
|
+
return { index, lineBreakLength: 1 };
|
|
62
|
+
}
|
|
63
|
+
if (character === "\r") {
|
|
64
|
+
return {
|
|
65
|
+
index,
|
|
66
|
+
lineBreakLength: source[index + 1] === "\n" ? 2 : 1
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
index += 1;
|
|
70
|
+
}
|
|
71
|
+
return { index: source.length, lineBreakLength: 0 };
|
|
72
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { splitFrontmatterBlock, type FrontmatterBlock, type SplitFrontmatterResult } from "./fences.js";
|
|
2
|
+
export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument, type ParsedFrontmatter, type ParsedFrontmatterDocument, type ParseFrontmatterOptions } from "./parse.js";
|
|
3
|
+
export { stringifyFrontmatter } from "./stringify.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LineCounter } from "yaml";
|
|
2
|
+
export interface ParsedFrontmatter {
|
|
3
|
+
frontmatter: Record<string, unknown>;
|
|
4
|
+
body: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedFrontmatterDocument extends ParsedFrontmatter {
|
|
7
|
+
errors: readonly {
|
|
8
|
+
message: string;
|
|
9
|
+
pos?: [number, number];
|
|
10
|
+
}[];
|
|
11
|
+
lineCounter: LineCounter;
|
|
12
|
+
}
|
|
13
|
+
export interface ParseFrontmatterOptions {
|
|
14
|
+
uniqueKeys?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class FrontmatterParseError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function parseFrontmatter(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatter;
|
|
20
|
+
export declare function parseFrontmatterDocument(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatterDocument;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { LineCounter, parse, parseDocument } from "yaml";
|
|
2
|
+
import { splitFrontmatterBlock } from "./fences.js";
|
|
3
|
+
export class FrontmatterParseError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "FrontmatterParseError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function parseFrontmatter(source, options = {}) {
|
|
10
|
+
const split = splitFrontmatter(source);
|
|
11
|
+
if (split.raw === undefined) {
|
|
12
|
+
return {
|
|
13
|
+
frontmatter: {},
|
|
14
|
+
body: split.body
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
frontmatter: parseYamlFrontmatter(split.raw, options),
|
|
19
|
+
body: split.body
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function parseFrontmatterDocument(source, options = {}) {
|
|
23
|
+
const split = splitFrontmatter(source);
|
|
24
|
+
const lineCounter = new LineCounter();
|
|
25
|
+
if (split.raw === undefined) {
|
|
26
|
+
return {
|
|
27
|
+
frontmatter: {},
|
|
28
|
+
body: split.body,
|
|
29
|
+
errors: [],
|
|
30
|
+
lineCounter
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const document = parseDocument(normalizeYamlLineEndings(split.raw), {
|
|
34
|
+
lineCounter,
|
|
35
|
+
prettyErrors: false,
|
|
36
|
+
uniqueKeys: options.uniqueKeys ?? false
|
|
37
|
+
});
|
|
38
|
+
const errors = document.errors.map((error) => ({
|
|
39
|
+
message: error.message,
|
|
40
|
+
...(error.pos === undefined ? {} : { pos: error.pos })
|
|
41
|
+
}));
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
frontmatter: {},
|
|
45
|
+
body: split.body,
|
|
46
|
+
errors,
|
|
47
|
+
lineCounter
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
frontmatter: normalizeYamlFrontmatter(document.toJSON()),
|
|
52
|
+
body: split.body,
|
|
53
|
+
errors,
|
|
54
|
+
lineCounter
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function splitFrontmatter(source) {
|
|
58
|
+
try {
|
|
59
|
+
return splitFrontmatterBlock(source);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
throw new FrontmatterParseError(error.message);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseYamlFrontmatter(yamlBlock, options) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = parse(normalizeYamlLineEndings(yamlBlock), {
|
|
72
|
+
uniqueKeys: options.uniqueKeys ?? false
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : "Unknown YAML parse error";
|
|
77
|
+
throw new FrontmatterParseError(`Invalid YAML frontmatter: ${message}`);
|
|
78
|
+
}
|
|
79
|
+
return normalizeYamlFrontmatter(parsed);
|
|
80
|
+
}
|
|
81
|
+
function normalizeYamlLineEndings(value) {
|
|
82
|
+
if (!value.includes("\r")) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
let normalized = "";
|
|
86
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
87
|
+
const character = value[index];
|
|
88
|
+
if (character !== "\r") {
|
|
89
|
+
normalized += character;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (value[index + 1] === "\n") {
|
|
93
|
+
normalized += "\r\n";
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
normalized += "\n";
|
|
98
|
+
}
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
function normalizeYamlFrontmatter(value) {
|
|
102
|
+
if (value === null || value === undefined) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
if (!isRecord(value)) {
|
|
106
|
+
throw new FrontmatterParseError("YAML frontmatter must parse to an object.");
|
|
107
|
+
}
|
|
108
|
+
return normalizeYamlValue(value);
|
|
109
|
+
}
|
|
110
|
+
function normalizeYamlValue(value) {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return value.map((item) => normalizeYamlValue(item));
|
|
113
|
+
}
|
|
114
|
+
if (!isPlainRecord(value)) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
const normalized = {};
|
|
118
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
119
|
+
Object.defineProperty(normalized, key, {
|
|
120
|
+
configurable: true,
|
|
121
|
+
enumerable: true,
|
|
122
|
+
value: normalizeYamlValue(entryValue),
|
|
123
|
+
writable: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
function isRecord(value) {
|
|
129
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
function isPlainRecord(value) {
|
|
132
|
+
if (!isRecord(value)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const prototype = Object.getPrototypeOf(value);
|
|
136
|
+
return prototype === Object.prototype || prototype === null;
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stringifyFrontmatter(frontmatter: Record<string, unknown>, body: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { stringify } from "yaml";
|
|
2
|
+
import { FrontmatterParseError } from "./parse.js";
|
|
3
|
+
export function stringifyFrontmatter(frontmatter, body) {
|
|
4
|
+
try {
|
|
5
|
+
assertAcyclic(frontmatter);
|
|
6
|
+
return `---\n${stringify(frontmatter, { aliasDuplicateObjects: false }).trimEnd()}\n---\n${body}`;
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof FrontmatterParseError) {
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
const message = error instanceof Error ? error.message : "Unknown YAML stringify error";
|
|
13
|
+
throw new FrontmatterParseError(`Invalid YAML frontmatter: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function assertAcyclic(value, seen = new WeakSet()) {
|
|
17
|
+
if (typeof value !== "object" || value === null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (seen.has(value)) {
|
|
21
|
+
throw new FrontmatterParseError("Cannot stringify cyclic frontmatter.");
|
|
22
|
+
}
|
|
23
|
+
seen.add(value);
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const item of value) {
|
|
26
|
+
assertAcyclic(item, seen);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
for (const item of Object.values(value)) {
|
|
31
|
+
assertAcyclic(item, seen);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
seen.delete(value);
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@poe-code/frontmatter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node ../../scripts/guard-package-dist.mjs && tsc",
|
|
16
|
+
"test": "cd ../.. && vitest run packages/frontmatter/src",
|
|
17
|
+
"test:unit": "cd ../.. && vitest run packages/frontmatter/src"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"yaml": "^2.8.2"
|
|
24
|
+
}
|
|
25
|
+
}
|