promptopskit 0.3.8 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +97 -10
  2. package/SKILL.md +69 -6
  3. package/dist/chunk-F6UJ73Y6.js +113 -0
  4. package/dist/chunk-F6UJ73Y6.js.map +1 -0
  5. package/dist/chunk-HLNVKKMS.js +145 -0
  6. package/dist/chunk-HLNVKKMS.js.map +1 -0
  7. package/dist/{chunk-5OYWNRNC.js → chunk-JBLUPKY4.js} +191 -5
  8. package/dist/chunk-JBLUPKY4.js.map +1 -0
  9. package/dist/{chunk-UJU5XERR.js → chunk-L2V7LDKD.js} +25 -4
  10. package/dist/chunk-L2V7LDKD.js.map +1 -0
  11. package/dist/{chunk-E6NPZOJL.js → chunk-RY5DVTLV.js} +29 -4
  12. package/dist/chunk-RY5DVTLV.js.map +1 -0
  13. package/dist/chunk-UPTCSWK3.js +60 -0
  14. package/dist/chunk-UPTCSWK3.js.map +1 -0
  15. package/dist/{chunk-X73VG645.js → chunk-XNNDGDPI.js} +64 -3
  16. package/dist/chunk-XNNDGDPI.js.map +1 -0
  17. package/dist/cli/index.js +423 -7
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/index.cjs +655 -22
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +7 -4
  22. package/dist/index.d.ts +7 -4
  23. package/dist/index.js +172 -11
  24. package/dist/index.js.map +1 -1
  25. package/dist/providers/anthropic.cjs +306 -10
  26. package/dist/providers/anthropic.cjs.map +1 -1
  27. package/dist/providers/anthropic.d.cts +2 -2
  28. package/dist/providers/anthropic.d.ts +2 -2
  29. package/dist/providers/anthropic.js +3 -3
  30. package/dist/providers/gemini.cjs +274 -6
  31. package/dist/providers/gemini.cjs.map +1 -1
  32. package/dist/providers/gemini.d.cts +2 -2
  33. package/dist/providers/gemini.d.ts +2 -2
  34. package/dist/providers/gemini.js +3 -3
  35. package/dist/providers/openai-responses.cjs +1245 -0
  36. package/dist/providers/openai-responses.cjs.map +1 -0
  37. package/dist/providers/openai-responses.d.cts +11 -0
  38. package/dist/providers/openai-responses.d.ts +11 -0
  39. package/dist/providers/openai-responses.js +9 -0
  40. package/dist/providers/openai-responses.js.map +1 -0
  41. package/dist/providers/openai.cjs +270 -6
  42. package/dist/providers/openai.cjs.map +1 -1
  43. package/dist/providers/openai.d.cts +2 -2
  44. package/dist/providers/openai.d.ts +2 -2
  45. package/dist/providers/openai.js +3 -3
  46. package/dist/providers/openrouter.cjs +305 -9
  47. package/dist/providers/openrouter.cjs.map +1 -1
  48. package/dist/providers/openrouter.d.cts +2 -2
  49. package/dist/providers/openrouter.d.ts +2 -2
  50. package/dist/providers/openrouter.js +4 -4
  51. package/dist/schema-B9A1zLRk.d.cts +2403 -0
  52. package/dist/schema-B9A1zLRk.d.ts +2403 -0
  53. package/dist/testing.cjs +264 -6
  54. package/dist/testing.cjs.map +1 -1
  55. package/dist/testing.d.cts +59 -2
  56. package/dist/testing.d.ts +59 -2
  57. package/dist/testing.js +70 -2
  58. package/dist/testing.js.map +1 -1
  59. package/dist/{types-fBvo_d4k.d.ts → types-DjGAwJxC.d.ts} +13 -1
  60. package/dist/{types-C6RG0Si0.d.cts → types-DpbzzZO2.d.cts} +13 -1
  61. package/dist/usagetap/index.d.cts +2 -2
  62. package/dist/usagetap/index.d.ts +2 -2
  63. package/package.json +12 -2
  64. package/dist/chunk-5OYWNRNC.js.map +0 -1
  65. package/dist/chunk-BBVVA27J.js +0 -92
  66. package/dist/chunk-BBVVA27J.js.map +0 -1
  67. package/dist/chunk-E2NXJLS6.js +0 -26
  68. package/dist/chunk-E2NXJLS6.js.map +0 -1
  69. package/dist/chunk-E6NPZOJL.js.map +0 -1
  70. package/dist/chunk-UJU5XERR.js.map +0 -1
  71. package/dist/chunk-X73VG645.js.map +0 -1
  72. package/dist/schema-DwzYbZba.d.cts +0 -859
  73. package/dist/schema-DwzYbZba.d.ts +0 -859
package/dist/cli/index.js CHANGED
@@ -39,7 +39,66 @@ var SamplingSchema = z.object({
39
39
  });
40
40
  var ResponseSchema = z.object({
41
41
  format: z.enum(["text", "json", "markdown"]).optional(),
42
- stream: z.boolean().optional()
42
+ stream: z.boolean().optional(),
43
+ schema: z.record(z.unknown()).optional(),
44
+ schema_name: z.string().optional(),
45
+ schema_description: z.string().optional(),
46
+ schema_strict: z.boolean().optional()
47
+ });
48
+ var AnthropicProviderOptionsSchema = z.object({
49
+ top_k: z.number().int().min(0).optional(),
50
+ tool_choice: z.record(z.unknown()).optional(),
51
+ output_config: z.record(z.unknown()).optional()
52
+ });
53
+ var GeminiProviderOptionsSchema = z.object({
54
+ candidate_count: z.number().int().positive().optional(),
55
+ top_k: z.number().int().min(0).optional(),
56
+ seed: z.number().int().optional(),
57
+ response_schema: z.record(z.unknown()).optional(),
58
+ response_json_schema: z.record(z.unknown()).optional(),
59
+ response_modalities: z.array(z.string()).optional(),
60
+ thinking_budget_tokens: z.number().int().positive().optional()
61
+ });
62
+ var OpenRouterProviderOptionsSchema = z.object({
63
+ provider: z.record(z.unknown()).optional(),
64
+ transforms: z.array(z.string()).optional(),
65
+ plugins: z.array(z.record(z.unknown())).optional(),
66
+ models: z.array(z.string()).optional()
67
+ });
68
+ var ProviderOptionsSchema = z.object({
69
+ anthropic: AnthropicProviderOptionsSchema.optional(),
70
+ gemini: GeminiProviderOptionsSchema.optional(),
71
+ openrouter: OpenRouterProviderOptionsSchema.optional()
72
+ });
73
+ var RawProviderBodySchema = z.object({
74
+ openai: z.record(z.unknown()).optional(),
75
+ "openai-responses": z.record(z.unknown()).optional(),
76
+ openai_responses: z.record(z.unknown()).optional(),
77
+ anthropic: z.record(z.unknown()).optional(),
78
+ gemini: z.record(z.unknown()).optional(),
79
+ google: z.record(z.unknown()).optional(),
80
+ openrouter: z.record(z.unknown()).optional()
81
+ });
82
+ var OpenAICacheSchema = z.object({
83
+ prompt_cache_key: z.string().min(1).optional(),
84
+ retention: z.enum(["in_memory", "24h"]).optional()
85
+ });
86
+ var AnthropicCacheSchema = z.object({
87
+ mode: z.enum(["automatic", "explicit"]).optional(),
88
+ type: z.literal("ephemeral").optional(),
89
+ ttl: z.enum(["5m", "1h"]).optional(),
90
+ cache_system_instructions: z.boolean().optional(),
91
+ cache_tools: z.boolean().optional(),
92
+ cache_prompt_template: z.boolean().optional()
93
+ });
94
+ var GeminiCacheSchema = z.object({
95
+ cached_content: z.string().min(1).optional()
96
+ });
97
+ var CacheSchema = z.object({
98
+ openai: OpenAICacheSchema.optional(),
99
+ anthropic: AnthropicCacheSchema.optional(),
100
+ gemini: GeminiCacheSchema.optional(),
101
+ google: GeminiCacheSchema.optional()
43
102
  });
44
103
  var HistorySchema = z.object({
45
104
  max_items: z.number().int().positive().optional()
@@ -90,7 +149,10 @@ var PromptAssetOverridesSchema = z.object({
90
149
  reasoning: ReasoningSchema.optional(),
91
150
  sampling: SamplingSchema.optional(),
92
151
  response: ResponseSchema.optional(),
93
- tools: z.array(ToolRefSchema).optional()
152
+ cache: CacheSchema.optional(),
153
+ raw: RawProviderBodySchema.optional(),
154
+ tools: z.array(ToolRefSchema).optional(),
155
+ provider_options: ProviderOptionsSchema.optional()
94
156
  });
95
157
  var SourceSchema = z.object({
96
158
  file_path: z.string().optional(),
@@ -102,8 +164,9 @@ var SectionsSchema = z.object({
102
164
  notes: z.string().optional()
103
165
  });
104
166
  var PromptDefaultsSchema = z.object({
105
- provider: z.enum(["openai", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
167
+ provider: z.enum(["openai", "openai-responses", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
106
168
  model: z.string().optional(),
169
+ cache: CacheSchema.optional(),
107
170
  metadata: MetadataSchema.optional(),
108
171
  sections: z.object({
109
172
  system_instructions: z.string().optional()
@@ -113,13 +176,16 @@ var PromptAssetSchema = z.object({
113
176
  id: z.string(),
114
177
  schema_version: z.number().int().positive().default(1),
115
178
  description: z.string().optional(),
116
- provider: z.enum(["openai", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
179
+ provider: z.enum(["openai", "openai-responses", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
117
180
  model: z.string().optional(),
118
181
  fallback_models: z.array(z.string()).optional(),
119
182
  reasoning: ReasoningSchema.optional(),
120
183
  sampling: SamplingSchema.optional(),
121
184
  response: ResponseSchema.optional(),
185
+ cache: CacheSchema.optional(),
186
+ raw: RawProviderBodySchema.optional(),
122
187
  tools: z.array(ToolRefSchema).optional(),
188
+ provider_options: ProviderOptionsSchema.optional(),
123
189
  mcp: MCPSchema.optional(),
124
190
  context: ContextSchema.optional(),
125
191
  includes: z.array(z.string()).optional(),
@@ -131,6 +197,123 @@ var PromptAssetSchema = z.object({
131
197
  source: SourceSchema.optional()
132
198
  });
133
199
 
200
+ // src/parser/frontmatter-guard.ts
201
+ var FRONT_MATTER_DELIMITER = /^---[ \t]*$/;
202
+ var REGEX_FIELD = /^(?<indent>\s*)(?<field>allow_regex|deny_regex)\s*:\s*(?<value>.*)$/;
203
+ var PATTERN_FIELD = /^(?<indent>\s*)pattern\s*:\s*(?<value>.*)$/;
204
+ var INLINE_PATTERN_FIELD = /(?:^|[{,]\s*)pattern\s*:\s*(?<value>"(?:\\.|[^"\\])*")/;
205
+ function assertRegexFrontMatterQuoting(content, filePath) {
206
+ const frontMatter = extractFrontMatter(content);
207
+ if (!frontMatter) {
208
+ return;
209
+ }
210
+ let pendingRegex;
211
+ for (const [index, line] of frontMatter.lines.entries()) {
212
+ const lineNumber = frontMatter.startLine + index;
213
+ const regexMatch = line.match(REGEX_FIELD);
214
+ if (regexMatch?.groups) {
215
+ const field = regexMatch.groups.field;
216
+ const indent2 = regexMatch.groups.indent.length;
217
+ const value = regexMatch.groups.value.trim();
218
+ pendingRegex = value === "" ? { field, indent: indent2 } : void 0;
219
+ assertNoUnescapedBackslashInDoubleQuotedRegex(value, field, filePath, lineNumber);
220
+ assertNoUnescapedBackslashInInlinePattern(value, field, filePath, lineNumber);
221
+ continue;
222
+ }
223
+ if (!pendingRegex) {
224
+ continue;
225
+ }
226
+ if (line.trim() === "" || line.trimStart().startsWith("#")) {
227
+ continue;
228
+ }
229
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
230
+ if (indent <= pendingRegex.indent) {
231
+ pendingRegex = void 0;
232
+ continue;
233
+ }
234
+ const patternMatch = line.match(PATTERN_FIELD);
235
+ if (!patternMatch?.groups) {
236
+ continue;
237
+ }
238
+ assertNoUnescapedBackslashInDoubleQuotedRegex(
239
+ patternMatch.groups.value.trim(),
240
+ `${pendingRegex.field}.pattern`,
241
+ filePath,
242
+ lineNumber
243
+ );
244
+ }
245
+ }
246
+ function assertNoUnescapedBackslashInInlinePattern(value, field, filePath, lineNumber) {
247
+ const match = value.match(INLINE_PATTERN_FIELD);
248
+ const pattern = match?.groups?.value;
249
+ if (!pattern) {
250
+ return;
251
+ }
252
+ assertNoUnescapedBackslashInDoubleQuotedRegex(pattern, `${field}.pattern`, filePath, lineNumber);
253
+ }
254
+ function extractFrontMatter(content) {
255
+ const lines = content.split(/\r?\n/);
256
+ if (!FRONT_MATTER_DELIMITER.test(lines[0] ?? "")) {
257
+ return void 0;
258
+ }
259
+ for (let index = 1; index < lines.length; index += 1) {
260
+ if (FRONT_MATTER_DELIMITER.test(lines[index] ?? "")) {
261
+ return {
262
+ lines: lines.slice(1, index),
263
+ startLine: 2
264
+ };
265
+ }
266
+ }
267
+ return void 0;
268
+ }
269
+ function assertNoUnescapedBackslashInDoubleQuotedRegex(value, field, filePath, lineNumber) {
270
+ if (!value.startsWith('"')) {
271
+ return;
272
+ }
273
+ const quoted = readDoubleQuotedScalar(value);
274
+ if (!quoted) {
275
+ return;
276
+ }
277
+ if (!hasUnescapedBackslash(quoted)) {
278
+ return;
279
+ }
280
+ const location = filePath ? `${filePath}:${lineNumber}` : `line ${lineNumber}`;
281
+ throw new Error(
282
+ `POK013: Invalid context regex YAML at ${location}, field "${field}": double-quoted regex strings treat backslashes as YAML escapes. Use unquoted /pattern/i literal form, single quotes, or double each backslash.`
283
+ );
284
+ }
285
+ function readDoubleQuotedScalar(value) {
286
+ let result = "";
287
+ for (let index = 1; index < value.length; index += 1) {
288
+ const char = value[index];
289
+ if (char === '"') {
290
+ return result;
291
+ }
292
+ result += char;
293
+ if (char === "\\" && index + 1 < value.length) {
294
+ index += 1;
295
+ result += value[index];
296
+ }
297
+ }
298
+ return void 0;
299
+ }
300
+ function hasUnescapedBackslash(value) {
301
+ for (let index = 0; index < value.length; index += 1) {
302
+ if (value[index] !== "\\") {
303
+ continue;
304
+ }
305
+ let count = 1;
306
+ while (value[index + count] === "\\") {
307
+ count += 1;
308
+ }
309
+ if (count % 2 === 1) {
310
+ return true;
311
+ }
312
+ index += count - 1;
313
+ }
314
+ return false;
315
+ }
316
+
134
317
  // src/parser/sections.ts
135
318
  var SECTION_MAP = {
136
319
  "system instructions": "system_instructions",
@@ -171,6 +354,7 @@ function extractSections(body) {
171
354
 
172
355
  // src/parser/parser.ts
173
356
  function parsePrompt(content, filePath) {
357
+ assertRegexFrontMatterQuoting(content, filePath);
174
358
  const { data: frontMatter, content: body } = matter(content);
175
359
  const sections = extractSections(body);
176
360
  const raw = {
@@ -496,8 +680,22 @@ var KNOWN_FRONT_MATTER_KEYS = /* @__PURE__ */ new Set([
496
680
  "includes",
497
681
  "environments",
498
682
  "tiers",
499
- "metadata"
683
+ "metadata",
684
+ "cache",
685
+ "provider_options",
686
+ "raw"
500
687
  ]);
688
+ var RISKY_UNBOUNDED_INPUT_NAMES = [
689
+ "message",
690
+ "prompt",
691
+ "history",
692
+ "transcript",
693
+ "document",
694
+ "content",
695
+ "input",
696
+ "body",
697
+ "context"
698
+ ];
501
699
  function validateAsset(asset, frontMatterKeys, filePath) {
502
700
  const errors = [];
503
701
  const warnings = [];
@@ -568,7 +766,32 @@ function validateAsset(asset, frontMatterKeys, filePath) {
568
766
  });
569
767
  }
570
768
  }
769
+ if (usedVars.size > 0 && (!asset.context?.inputs || asset.context.inputs.length === 0)) {
770
+ warnings.push({
771
+ code: "POK046",
772
+ message: `Template uses ${usedVars.size === 1 ? "a variable" : "variables"} but context.inputs is not declared.`,
773
+ filePath,
774
+ suggestion: "Declare context.inputs to enable input policy validation."
775
+ });
776
+ }
571
777
  for (const input of getContextInputs(asset)) {
778
+ const lowerName = input.name.toLowerCase();
779
+ if (input.max_size === void 0 && RISKY_UNBOUNDED_INPUT_NAMES.some((needle) => lowerName.includes(needle))) {
780
+ warnings.push({
781
+ code: "POK040",
782
+ message: `Context input "${input.name}" has no max_size and appears unbounded.`,
783
+ filePath,
784
+ suggestion: "Add max_size to constrain prompt payload growth."
785
+ });
786
+ }
787
+ if (input.allow_regex === void 0 && input.deny_regex === void 0 && input.non_empty === void 0 && input.reject_secrets === void 0) {
788
+ warnings.push({
789
+ code: "POK041",
790
+ message: `Context input "${input.name}" has no input hardening validators.`,
791
+ filePath,
792
+ suggestion: "Consider non_empty/reject_secrets and allow/deny regex validators."
793
+ });
794
+ }
572
795
  if (input.trim !== void 0 && input.trim !== false && input.max_size === void 0) {
573
796
  warnings.push({
574
797
  code: "POK014",
@@ -596,6 +819,91 @@ function validateAsset(asset, frontMatterKeys, filePath) {
596
819
  }
597
820
  }
598
821
  }
822
+ if (asset.provider) {
823
+ let providerCache;
824
+ let cacheSuggestionField;
825
+ switch (asset.provider) {
826
+ case "openai":
827
+ providerCache = asset.cache?.openai;
828
+ cacheSuggestionField = "cache.openai";
829
+ break;
830
+ case "anthropic":
831
+ providerCache = asset.cache?.anthropic;
832
+ cacheSuggestionField = "cache.anthropic";
833
+ break;
834
+ case "gemini":
835
+ case "google":
836
+ providerCache = asset.cache?.gemini ?? asset.cache?.google;
837
+ cacheSuggestionField = "cache.gemini";
838
+ break;
839
+ default:
840
+ break;
841
+ }
842
+ if (cacheSuggestionField && providerCache === void 0) {
843
+ warnings.push({
844
+ code: "POK042",
845
+ message: `Provider "${asset.provider}" has no provider-specific cache settings.`,
846
+ filePath,
847
+ suggestion: `Consider configuring ${cacheSuggestionField} for better cache-hit behavior.`
848
+ });
849
+ }
850
+ if (!asset.model) {
851
+ warnings.push({
852
+ code: "POK044",
853
+ message: `Provider "${asset.provider}" is configured without a model.`,
854
+ filePath,
855
+ suggestion: "Set model in prompt or defaults to avoid adapter-time errors."
856
+ });
857
+ }
858
+ }
859
+ if (asset.cache?.gemini?.cached_content && asset.cache.google?.cached_content && asset.cache.gemini.cached_content !== asset.cache.google.cached_content) {
860
+ warnings.push({
861
+ code: "POK043",
862
+ message: "cache.gemini.cached_content and cache.google.cached_content are both set to different values.",
863
+ filePath,
864
+ suggestion: "Use one canonical value; Gemini prefers cache.gemini.cached_content."
865
+ });
866
+ }
867
+ for (const [envName, overrides] of Object.entries(asset.environments ?? {})) {
868
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
869
+ warnings.push({
870
+ code: "POK045",
871
+ message: `Environment "${envName}" does not override cache while prompt-level cache is defined.`,
872
+ filePath,
873
+ suggestion: "Confirm cache strategy is intentionally shared across environments."
874
+ });
875
+ }
876
+ }
877
+ for (const [tierName, overrides] of Object.entries(asset.tiers ?? {})) {
878
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
879
+ warnings.push({
880
+ code: "POK045",
881
+ message: `Tier "${tierName}" does not override cache while prompt-level cache is defined.`,
882
+ filePath,
883
+ suggestion: "Confirm cache strategy is intentionally shared across tiers."
884
+ });
885
+ }
886
+ }
887
+ for (const tool of asset.tools ?? []) {
888
+ if (isRecord(tool)) {
889
+ if (!tool.description) {
890
+ warnings.push({
891
+ code: "POK047",
892
+ message: `Inline tool "${tool.name}" is missing a description.`,
893
+ filePath,
894
+ suggestion: "Add description to improve model tool-selection quality."
895
+ });
896
+ }
897
+ if (!tool.input_schema) {
898
+ warnings.push({
899
+ code: "POK047",
900
+ message: `Inline tool "${tool.name}" is missing input_schema.`,
901
+ filePath,
902
+ suggestion: "Add input_schema so tool inputs are strongly typed."
903
+ });
904
+ }
905
+ }
906
+ }
599
907
  return {
600
908
  valid: errors.length === 0,
601
909
  errors,
@@ -632,6 +940,9 @@ function findClosestMatch(input, candidates) {
632
940
  }
633
941
  return best;
634
942
  }
943
+ function isRecord(value) {
944
+ return typeof value === "object" && value !== null && !Array.isArray(value);
945
+ }
635
946
 
636
947
  // src/prompt-resolution.ts
637
948
  import { readFile as readFile3 } from "fs/promises";
@@ -712,8 +1023,53 @@ function mergeOverride(base, override) {
712
1023
  if (override.response !== void 0) {
713
1024
  result.response = { ...result.response, ...override.response };
714
1025
  }
1026
+ if (override.cache !== void 0) {
1027
+ result.cache = {
1028
+ ...result.cache,
1029
+ ...override.cache,
1030
+ openai: mergeRecordBlock(result.cache?.openai, override.cache.openai),
1031
+ anthropic: mergeRecordBlock(result.cache?.anthropic, override.cache.anthropic),
1032
+ gemini: mergeRecordBlock(result.cache?.gemini, override.cache.gemini),
1033
+ google: mergeRecordBlock(result.cache?.google, override.cache.google)
1034
+ };
1035
+ }
1036
+ if (override.raw !== void 0) {
1037
+ result.raw = {
1038
+ ...result.raw,
1039
+ ...override.raw,
1040
+ openai: mergeRecordBlock(result.raw?.openai, override.raw.openai),
1041
+ "openai-responses": mergeRecordBlock(result.raw?.["openai-responses"], override.raw["openai-responses"]),
1042
+ openai_responses: mergeRecordBlock(result.raw?.openai_responses, override.raw.openai_responses),
1043
+ anthropic: mergeRecordBlock(result.raw?.anthropic, override.raw.anthropic),
1044
+ gemini: mergeRecordBlock(result.raw?.gemini, override.raw.gemini),
1045
+ google: mergeRecordBlock(result.raw?.google, override.raw.google),
1046
+ openrouter: mergeRecordBlock(result.raw?.openrouter, override.raw.openrouter)
1047
+ };
1048
+ }
1049
+ if (override.provider_options !== void 0) {
1050
+ result.provider_options = {
1051
+ ...result.provider_options,
1052
+ ...override.provider_options,
1053
+ anthropic: {
1054
+ ...result.provider_options?.anthropic,
1055
+ ...override.provider_options.anthropic
1056
+ },
1057
+ gemini: {
1058
+ ...result.provider_options?.gemini,
1059
+ ...override.provider_options.gemini
1060
+ },
1061
+ openrouter: {
1062
+ ...result.provider_options?.openrouter,
1063
+ ...override.provider_options.openrouter
1064
+ }
1065
+ };
1066
+ }
715
1067
  return result;
716
1068
  }
1069
+ function mergeRecordBlock(base, override) {
1070
+ if (override === void 0) return base;
1071
+ return { ...base, ...override };
1072
+ }
717
1073
 
718
1074
  // src/prompt-resolution.ts
719
1075
  var DEFAULT_PROMPTS_DIR = "./prompts";
@@ -1097,7 +1453,7 @@ async function inspect(args) {
1097
1453
 
1098
1454
  // src/cli/commands/init.ts
1099
1455
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1100
- import { join as join5, dirname as dirname5 } from "path";
1456
+ import { join as join5, dirname as dirname5, relative as relative2 } from "path";
1101
1457
  import { existsSync as existsSync4, readFileSync } from "fs";
1102
1458
  var HELP5 = `
1103
1459
  promptopskit init [dir]
@@ -1118,6 +1474,11 @@ context:
1118
1474
  - app_context
1119
1475
  includes:
1120
1476
  - ./shared/tone.md
1477
+ cache:
1478
+ openai:
1479
+ # Keep this stable across requests that share a long static prefix.
1480
+ prompt_cache_key: hello-v1
1481
+ retention: in_memory
1121
1482
  reasoning:
1122
1483
  effort: high
1123
1484
  environments:
@@ -1164,10 +1525,14 @@ var TEST_SIDECAR = `cases:
1164
1525
  variables:
1165
1526
  name: "World"
1166
1527
  app_context: "Welcome screen"
1528
+ response:
1529
+ message: "Hello, World! How can I help you today?"
1167
1530
  - name: named-greeting
1168
1531
  variables:
1169
1532
  name: "Alice"
1170
1533
  app_context: "Settings page"
1534
+ response:
1535
+ message: "Hello, Alice! How can I help you today?"
1171
1536
  `;
1172
1537
  var EXAMPLE_USAGE = `// Example: render the hello prompt and send it to OpenAI
1173
1538
  // Full docs: https://promptopskit.com/docs/index.html#/
@@ -1239,12 +1604,16 @@ async function init(args) {
1239
1604
  return;
1240
1605
  }
1241
1606
  const dir = args.find((a) => !a.startsWith("--")) ?? "./prompts";
1607
+ const testFilePath = join5(dirname5(dir), "tests", "hello.prompt.test.mjs");
1608
+ const promptsDirFromTest = relative2(dirname5(testFilePath), dir) || ".";
1609
+ const helloPromptTest = createHelloPromptTest(promptsDirFromTest);
1242
1610
  const files = [
1243
1611
  { path: join5(dir, "defaults.md"), content: DEFAULTS },
1244
1612
  { path: join5(dir, "hello.md"), content: HELLO_PROMPT },
1245
1613
  { path: join5(dir, "hello.test.yaml"), content: TEST_SIDECAR },
1246
1614
  { path: join5(dir, "shared", "tone.md"), content: TONE_INCLUDE },
1247
- { path: join5(dir, "example-usage.ts"), content: EXAMPLE_USAGE }
1615
+ { path: join5(dir, "example-usage.ts"), content: EXAMPLE_USAGE },
1616
+ { path: testFilePath, content: helloPromptTest }
1248
1617
  ];
1249
1618
  let created = 0;
1250
1619
  let skipped = 0;
@@ -1269,10 +1638,57 @@ async function init(args) {
1269
1638
  console.log(`Tip: Add to your package.json scripts:`);
1270
1639
  console.log(` "build:prompts": "promptopskit compile ${dir}"`);
1271
1640
  }
1641
+ if (!pkg.scripts?.test) {
1642
+ console.log();
1643
+ console.log(`Tip: Add a test script to run the generated prompt test:`);
1644
+ console.log(` "test": "node --test tests/*.test.mjs"`);
1645
+ }
1272
1646
  } catch {
1273
1647
  }
1274
1648
  }
1275
1649
  }
1650
+ function createHelloPromptTest(promptsDirFromTest) {
1651
+ return `import assert from 'node:assert/strict';
1652
+ import { dirname, resolve } from 'node:path';
1653
+ import test from 'node:test';
1654
+ import { fileURLToPath } from 'node:url';
1655
+ import { createPromptOpsKit } from 'promptopskit';
1656
+ import {
1657
+ getHardcodedPromptResponse,
1658
+ loadPromptTestSidecar,
1659
+ renderPromptTestCase,
1660
+ } from 'promptopskit/testing';
1661
+
1662
+ const testDir = dirname(fileURLToPath(import.meta.url));
1663
+ const promptsDir = resolve(testDir, ${JSON.stringify(promptsDirFromTest)});
1664
+
1665
+ test('hello prompt renders every sidecar case', async () => {
1666
+ const kit = createPromptOpsKit({ sourceDir: promptsDir, cache: false });
1667
+ const sidecar = await loadPromptTestSidecar(resolve(promptsDir, 'hello.test.yaml'));
1668
+
1669
+ for (const testCase of sidecar.cases) {
1670
+ const { rendered } = await renderPromptTestCase(kit, {
1671
+ sidecar,
1672
+ caseName: testCase.name,
1673
+ path: 'hello',
1674
+ provider: 'openai',
1675
+ environment: 'dev',
1676
+ strict: true,
1677
+ });
1678
+
1679
+ assert.ok(rendered.request?.body?.messages);
1680
+ }
1681
+ });
1682
+
1683
+ test('hello prompt can return a deterministic response without calling a model', async () => {
1684
+ const sidecar = await loadPromptTestSidecar(resolve(promptsDir, 'hello.test.yaml'));
1685
+
1686
+ assert.deepEqual(getHardcodedPromptResponse(sidecar, 'basic-greeting'), {
1687
+ message: 'Hello, World! How can I help you today?',
1688
+ });
1689
+ });
1690
+ `;
1691
+ }
1276
1692
 
1277
1693
  // src/cli/commands/skill.ts
1278
1694
  import { writeFile as writeFile3, readFile as readFile5, mkdir as mkdir3 } from "fs/promises";