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.
Files changed (61) hide show
  1. package/README.md +41 -4
  2. package/dist/cli.js +40 -9
  3. package/dist/http-errors.d.ts +57 -0
  4. package/dist/http-errors.js +72 -0
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.js +15 -0
  7. package/dist/mcp-proxy.js +16 -1
  8. package/dist/mcp.js +224 -12
  9. package/node_modules/@poe-code/frontmatter/README.md +35 -0
  10. package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
  11. package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
  12. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
  13. package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
  14. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
  15. package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
  16. package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
  17. package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
  18. package/node_modules/@poe-code/frontmatter/package.json +25 -0
  19. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +1 -0
  20. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +25 -1
  21. package/node_modules/tiny-mcp-client/README.md +7 -0
  22. package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
  23. package/node_modules/tiny-mcp-client/dist/internal.js +3 -0
  24. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +10 -0
  25. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +2 -8
  26. package/node_modules/tiny-mcp-client/src/internal.ts +6 -0
  27. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +10 -0
  28. package/node_modules/toolcraft-design/README.md +12 -0
  29. package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
  30. package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
  31. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
  32. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
  33. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
  34. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
  35. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
  36. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
  37. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
  38. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
  39. package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
  40. package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
  41. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
  42. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
  43. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
  44. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
  45. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
  46. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
  47. package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
  48. package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
  49. package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
  50. package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
  51. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
  52. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
  53. package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
  54. package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
  55. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
  56. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
  57. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
  58. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
  59. package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
  60. package/node_modules/toolcraft-design/package.json +6 -3
  61. 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
- items: applySchemaCasing(schema.items, casing)
139
+ ...(additionalProperties === undefined ? {} : { additionalProperties }),
140
+ items: applySchemaCasing(schema.items, casing),
141
+ ...(oneOf === undefined ? {} : { oneOf })
136
142
  };
137
143
  }
138
- return schema;
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
- return [{ type: "text", text: JSON.stringify(result) }];
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
- server.tool(tool.name, tool.description, tool.inputSchema, async (argumentsValue) => {
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,8 @@
1
+ export type FrontmatterBlock = {
2
+ raw: string;
3
+ body: string;
4
+ };
5
+ export type SplitFrontmatterResult = FrontmatterBlock | {
6
+ body: string;
7
+ };
8
+ export declare function splitFrontmatterBlock(source: string): SplitFrontmatterResult;
@@ -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,3 @@
1
+ export { splitFrontmatterBlock } from "./fences.js";
2
+ export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument } 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
+ }
@@ -3,6 +3,7 @@ export interface JwksTokenVerifierOptions {
3
3
  jwksUrl: string | URL;
4
4
  clockSkewSeconds?: number;
5
5
  allowedAlgorithms?: readonly string[];
6
+ jwksCacheTtlMs?: number;
6
7
  fetch?: FetchLike;
7
8
  }
8
9
  export interface JwksVerifiedAccessToken {