promptopskit 0.3.9 → 0.4.2

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 (70) hide show
  1. package/README.md +91 -7
  2. package/SKILL.md +69 -6
  3. package/dist/chunk-5AAYDOPU.js +60 -0
  4. package/dist/chunk-5AAYDOPU.js.map +1 -0
  5. package/dist/{chunk-YNIAA4ZJ.js → chunk-7MRMN3SW.js} +5 -3
  6. package/dist/chunk-7MRMN3SW.js.map +1 -0
  7. package/dist/{chunk-4LEPCCSA.js → chunk-JKBWC5WJ.js} +176 -4
  8. package/dist/chunk-JKBWC5WJ.js.map +1 -0
  9. package/dist/{chunk-AN3OR7YC.js → chunk-MI3M6KDD.js} +7 -5
  10. package/dist/chunk-MI3M6KDD.js.map +1 -0
  11. package/dist/{chunk-LMURMZUA.js → chunk-XL76NHZR.js} +5 -3
  12. package/dist/chunk-XL76NHZR.js.map +1 -0
  13. package/dist/{chunk-V7QY7MRM.js → chunk-YI4LILPY.js} +50 -3
  14. package/dist/chunk-YI4LILPY.js.map +1 -0
  15. package/dist/{chunk-O6RQZYS4.js → chunk-Z4IPWEUU.js} +16 -5
  16. package/dist/chunk-Z4IPWEUU.js.map +1 -0
  17. package/dist/cli/index.js +440 -52
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/index.cjs +430 -17
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +4 -4
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +162 -10
  24. package/dist/index.js.map +1 -1
  25. package/dist/providers/anthropic.cjs +232 -6
  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 +223 -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 +221 -4
  36. package/dist/providers/openai-responses.cjs.map +1 -1
  37. package/dist/providers/openai-responses.d.cts +2 -2
  38. package/dist/providers/openai-responses.d.ts +2 -2
  39. package/dist/providers/openai-responses.js +3 -3
  40. package/dist/providers/openai.cjs +221 -4
  41. package/dist/providers/openai.cjs.map +1 -1
  42. package/dist/providers/openai.d.cts +2 -2
  43. package/dist/providers/openai.d.ts +2 -2
  44. package/dist/providers/openai.js +3 -3
  45. package/dist/providers/openrouter.cjs +256 -7
  46. package/dist/providers/openrouter.cjs.map +1 -1
  47. package/dist/providers/openrouter.d.cts +2 -2
  48. package/dist/providers/openrouter.d.ts +2 -2
  49. package/dist/providers/openrouter.js +4 -4
  50. package/dist/{schema-BKkg93rL.d.cts → schema-B9A1zLRk.d.cts} +482 -2
  51. package/dist/{schema-BKkg93rL.d.ts → schema-B9A1zLRk.d.ts} +482 -2
  52. package/dist/testing.cjs +217 -4
  53. package/dist/testing.cjs.map +1 -1
  54. package/dist/testing.d.cts +59 -2
  55. package/dist/testing.d.ts +59 -2
  56. package/dist/testing.js +70 -2
  57. package/dist/testing.js.map +1 -1
  58. package/dist/{types-CsQ8tON_.d.ts → types-DjGAwJxC.d.ts} +1 -1
  59. package/dist/{types-7cC17GWp.d.cts → types-DpbzzZO2.d.cts} +1 -1
  60. package/dist/usagetap/index.d.cts +2 -2
  61. package/dist/usagetap/index.d.ts +2 -2
  62. package/package.json +2 -2
  63. package/dist/chunk-4LEPCCSA.js.map +0 -1
  64. package/dist/chunk-AN3OR7YC.js.map +0 -1
  65. package/dist/chunk-LMURMZUA.js.map +0 -1
  66. package/dist/chunk-O6RQZYS4.js.map +0 -1
  67. package/dist/chunk-V7QY7MRM.js.map +0 -1
  68. package/dist/chunk-YAD6COQX.js +0 -26
  69. package/dist/chunk-YAD6COQX.js.map +0 -1
  70. package/dist/chunk-YNIAA4ZJ.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { readFileSync as readFileSync2 } from "fs";
5
5
 
6
6
  // src/cli/commands/validate.ts
7
- import { readdir } from "fs/promises";
8
- import { join as join2, extname } from "path";
7
+ import { readdir, stat } from "fs/promises";
8
+ import { join as join3, extname } from "path";
9
9
 
10
10
  // src/parser/parser.ts
11
11
  import matter from "gray-matter";
@@ -42,23 +42,42 @@ var ResponseSchema = z.object({
42
42
  stream: z.boolean().optional(),
43
43
  schema: z.record(z.unknown()).optional(),
44
44
  schema_name: z.string().optional(),
45
+ schema_description: z.string().optional(),
45
46
  schema_strict: z.boolean().optional()
46
47
  });
47
48
  var AnthropicProviderOptionsSchema = z.object({
48
49
  top_k: z.number().int().min(0).optional(),
49
- tool_choice: z.record(z.unknown()).optional()
50
+ tool_choice: z.record(z.unknown()).optional(),
51
+ output_config: z.record(z.unknown()).optional()
50
52
  });
51
53
  var GeminiProviderOptionsSchema = z.object({
52
54
  candidate_count: z.number().int().positive().optional(),
53
55
  top_k: z.number().int().min(0).optional(),
54
56
  seed: z.number().int().optional(),
55
57
  response_schema: z.record(z.unknown()).optional(),
58
+ response_json_schema: z.record(z.unknown()).optional(),
56
59
  response_modalities: z.array(z.string()).optional(),
57
60
  thinking_budget_tokens: z.number().int().positive().optional()
58
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
+ });
59
68
  var ProviderOptionsSchema = z.object({
60
69
  anthropic: AnthropicProviderOptionsSchema.optional(),
61
- gemini: GeminiProviderOptionsSchema.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()
62
81
  });
63
82
  var OpenAICacheSchema = z.object({
64
83
  prompt_cache_key: z.string().min(1).optional(),
@@ -131,6 +150,7 @@ var PromptAssetOverridesSchema = z.object({
131
150
  sampling: SamplingSchema.optional(),
132
151
  response: ResponseSchema.optional(),
133
152
  cache: CacheSchema.optional(),
153
+ raw: RawProviderBodySchema.optional(),
134
154
  tools: z.array(ToolRefSchema).optional(),
135
155
  provider_options: ProviderOptionsSchema.optional()
136
156
  });
@@ -163,6 +183,7 @@ var PromptAssetSchema = z.object({
163
183
  sampling: SamplingSchema.optional(),
164
184
  response: ResponseSchema.optional(),
165
185
  cache: CacheSchema.optional(),
186
+ raw: RawProviderBodySchema.optional(),
166
187
  tools: z.array(ToolRefSchema).optional(),
167
188
  provider_options: ProviderOptionsSchema.optional(),
168
189
  mcp: MCPSchema.optional(),
@@ -176,6 +197,123 @@ var PromptAssetSchema = z.object({
176
197
  source: SourceSchema.optional()
177
198
  });
178
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
+
179
317
  // src/parser/sections.ts
180
318
  var SECTION_MAP = {
181
319
  "system instructions": "system_instructions",
@@ -216,6 +354,7 @@ function extractSections(body) {
216
354
 
217
355
  // src/parser/parser.ts
218
356
  function parsePrompt(content, filePath) {
357
+ assertRegexFrontMatterQuoting(content, filePath);
219
358
  const { data: frontMatter, content: body } = matter(content);
220
359
  const sections = extractSections(body);
221
360
  const raw = {
@@ -293,6 +432,7 @@ function mergeDefaults(base, local) {
293
432
  return {
294
433
  provider: local.provider ?? base.provider,
295
434
  model: local.model ?? base.model,
435
+ cache: mergeCache(base.cache, local.cache),
296
436
  metadata: {
297
437
  ...base.metadata ?? {},
298
438
  ...local.metadata ?? {}
@@ -304,10 +444,12 @@ function mergeDefaults(base, local) {
304
444
  };
305
445
  }
306
446
  function applyDefaults(asset, defaults) {
447
+ const cache = mergeCache(defaults.cache, asset.cache);
307
448
  const hasDefaultMetadata = defaults.metadata && Object.keys(defaults.metadata).length > 0;
308
449
  const hasDefaultSystem = !!defaults.sections?.system_instructions;
309
450
  const hasDefaultScalars = defaults.provider !== void 0 || defaults.model !== void 0;
310
- if (!hasDefaultMetadata && !hasDefaultSystem && !hasDefaultScalars) {
451
+ const hasDefaultCache = cache !== void 0;
452
+ if (!hasDefaultMetadata && !hasDefaultSystem && !hasDefaultScalars && !hasDefaultCache) {
311
453
  return asset;
312
454
  }
313
455
  const mergedMetadata = {
@@ -321,10 +463,38 @@ function applyDefaults(asset, defaults) {
321
463
  ...asset,
322
464
  provider: asset.provider ?? defaults.provider,
323
465
  model: asset.model ?? defaults.model,
466
+ cache,
324
467
  metadata,
325
468
  sections
326
469
  };
327
470
  }
471
+ function mergeCache(base, local) {
472
+ const merged = {
473
+ ...base ?? {},
474
+ ...local ?? {},
475
+ openai: {
476
+ ...base?.openai ?? {},
477
+ ...local?.openai ?? {}
478
+ },
479
+ anthropic: {
480
+ ...base?.anthropic ?? {},
481
+ ...local?.anthropic ?? {}
482
+ },
483
+ gemini: {
484
+ ...base?.gemini ?? {},
485
+ ...local?.gemini ?? {}
486
+ },
487
+ google: {
488
+ ...base?.google ?? {},
489
+ ...local?.google ?? {}
490
+ }
491
+ };
492
+ if (merged.openai && Object.keys(merged.openai).length === 0) delete merged.openai;
493
+ if (merged.anthropic && Object.keys(merged.anthropic).length === 0) delete merged.anthropic;
494
+ if (merged.gemini && Object.keys(merged.gemini).length === 0) delete merged.gemini;
495
+ if (merged.google && Object.keys(merged.google).length === 0) delete merged.google;
496
+ return Object.keys(merged).length > 0 ? merged : void 0;
497
+ }
328
498
 
329
499
  // src/renderer/interpolate.ts
330
500
  var VARIABLE_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
@@ -542,8 +712,21 @@ var KNOWN_FRONT_MATTER_KEYS = /* @__PURE__ */ new Set([
542
712
  "environments",
543
713
  "tiers",
544
714
  "metadata",
545
- "cache"
715
+ "cache",
716
+ "provider_options",
717
+ "raw"
546
718
  ]);
719
+ var RISKY_UNBOUNDED_INPUT_NAMES = [
720
+ "message",
721
+ "prompt",
722
+ "history",
723
+ "transcript",
724
+ "document",
725
+ "content",
726
+ "input",
727
+ "body",
728
+ "context"
729
+ ];
547
730
  function validateAsset(asset, frontMatterKeys, filePath) {
548
731
  const errors = [];
549
732
  const warnings = [];
@@ -614,7 +797,32 @@ function validateAsset(asset, frontMatterKeys, filePath) {
614
797
  });
615
798
  }
616
799
  }
800
+ if (usedVars.size > 0 && (!asset.context?.inputs || asset.context.inputs.length === 0)) {
801
+ warnings.push({
802
+ code: "POK046",
803
+ message: `Template uses ${usedVars.size === 1 ? "a variable" : "variables"} but context.inputs is not declared.`,
804
+ filePath,
805
+ suggestion: "Declare context.inputs to enable input policy validation."
806
+ });
807
+ }
617
808
  for (const input of getContextInputs(asset)) {
809
+ const lowerName = input.name.toLowerCase();
810
+ if (input.max_size === void 0 && RISKY_UNBOUNDED_INPUT_NAMES.some((needle) => lowerName.includes(needle))) {
811
+ warnings.push({
812
+ code: "POK040",
813
+ message: `Context input "${input.name}" has no max_size and appears unbounded.`,
814
+ filePath,
815
+ suggestion: "Add max_size to constrain prompt payload growth."
816
+ });
817
+ }
818
+ if (input.allow_regex === void 0 && input.deny_regex === void 0 && input.non_empty === void 0 && input.reject_secrets === void 0) {
819
+ warnings.push({
820
+ code: "POK041",
821
+ message: `Context input "${input.name}" has no input hardening validators.`,
822
+ filePath,
823
+ suggestion: "Consider non_empty/reject_secrets and allow/deny regex validators."
824
+ });
825
+ }
618
826
  if (input.trim !== void 0 && input.trim !== false && input.max_size === void 0) {
619
827
  warnings.push({
620
828
  code: "POK014",
@@ -642,6 +850,91 @@ function validateAsset(asset, frontMatterKeys, filePath) {
642
850
  }
643
851
  }
644
852
  }
853
+ if (asset.provider) {
854
+ let providerCache;
855
+ let cacheSuggestionField;
856
+ switch (asset.provider) {
857
+ case "openai":
858
+ providerCache = asset.cache?.openai;
859
+ cacheSuggestionField = "cache.openai";
860
+ break;
861
+ case "anthropic":
862
+ providerCache = asset.cache?.anthropic;
863
+ cacheSuggestionField = "cache.anthropic";
864
+ break;
865
+ case "gemini":
866
+ case "google":
867
+ providerCache = asset.cache?.gemini ?? asset.cache?.google;
868
+ cacheSuggestionField = "cache.gemini";
869
+ break;
870
+ default:
871
+ break;
872
+ }
873
+ if (cacheSuggestionField && providerCache === void 0) {
874
+ warnings.push({
875
+ code: "POK042",
876
+ message: `Provider "${asset.provider}" has no provider-specific cache settings.`,
877
+ filePath,
878
+ suggestion: `Consider configuring ${cacheSuggestionField} for better cache-hit behavior.`
879
+ });
880
+ }
881
+ if (!asset.model) {
882
+ warnings.push({
883
+ code: "POK044",
884
+ message: `Provider "${asset.provider}" is configured without a model.`,
885
+ filePath,
886
+ suggestion: "Set model in prompt or defaults to avoid adapter-time errors."
887
+ });
888
+ }
889
+ }
890
+ if (asset.cache?.gemini?.cached_content && asset.cache.google?.cached_content && asset.cache.gemini.cached_content !== asset.cache.google.cached_content) {
891
+ warnings.push({
892
+ code: "POK043",
893
+ message: "cache.gemini.cached_content and cache.google.cached_content are both set to different values.",
894
+ filePath,
895
+ suggestion: "Use one canonical value; Gemini prefers cache.gemini.cached_content."
896
+ });
897
+ }
898
+ for (const [envName, overrides] of Object.entries(asset.environments ?? {})) {
899
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
900
+ warnings.push({
901
+ code: "POK045",
902
+ message: `Environment "${envName}" does not override cache while prompt-level cache is defined.`,
903
+ filePath,
904
+ suggestion: "Confirm cache strategy is intentionally shared across environments."
905
+ });
906
+ }
907
+ }
908
+ for (const [tierName, overrides] of Object.entries(asset.tiers ?? {})) {
909
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
910
+ warnings.push({
911
+ code: "POK045",
912
+ message: `Tier "${tierName}" does not override cache while prompt-level cache is defined.`,
913
+ filePath,
914
+ suggestion: "Confirm cache strategy is intentionally shared across tiers."
915
+ });
916
+ }
917
+ }
918
+ for (const tool of asset.tools ?? []) {
919
+ if (isRecord(tool)) {
920
+ if (!tool.description) {
921
+ warnings.push({
922
+ code: "POK047",
923
+ message: `Inline tool "${tool.name}" is missing a description.`,
924
+ filePath,
925
+ suggestion: "Add description to improve model tool-selection quality."
926
+ });
927
+ }
928
+ if (!tool.input_schema) {
929
+ warnings.push({
930
+ code: "POK047",
931
+ message: `Inline tool "${tool.name}" is missing input_schema.`,
932
+ filePath,
933
+ suggestion: "Add input_schema so tool inputs are strongly typed."
934
+ });
935
+ }
936
+ }
937
+ }
645
938
  return {
646
939
  valid: errors.length === 0,
647
940
  errors,
@@ -678,6 +971,9 @@ function findClosestMatch(input, candidates) {
678
971
  }
679
972
  return best;
680
973
  }
974
+ function isRecord(value) {
975
+ return typeof value === "object" && value !== null && !Array.isArray(value);
976
+ }
681
977
 
682
978
  // src/prompt-resolution.ts
683
979
  import { readFile as readFile3 } from "fs/promises";
@@ -696,8 +992,8 @@ var PromptCache = class {
696
992
  const entry = this.cache.get(filePath);
697
993
  if (!entry) return void 0;
698
994
  try {
699
- const stat = statSync(filePath);
700
- if (stat.mtimeMs !== entry.mtime) {
995
+ const stat2 = statSync(filePath);
996
+ if (stat2.mtimeMs !== entry.mtime) {
701
997
  this.cache.delete(filePath);
702
998
  return void 0;
703
999
  }
@@ -711,14 +1007,14 @@ var PromptCache = class {
711
1007
  }
712
1008
  set(filePath, value) {
713
1009
  try {
714
- const stat = statSync(filePath);
1010
+ const stat2 = statSync(filePath);
715
1011
  if (this.cache.has(filePath)) {
716
1012
  this.cache.delete(filePath);
717
1013
  } else if (this.cache.size >= this.maxSize) {
718
1014
  const oldest = this.cache.keys().next().value;
719
1015
  if (oldest) this.cache.delete(oldest);
720
1016
  }
721
- this.cache.set(filePath, { value, mtime: stat.mtimeMs });
1017
+ this.cache.set(filePath, { value, mtime: stat2.mtimeMs });
722
1018
  } catch {
723
1019
  }
724
1020
  }
@@ -758,6 +1054,29 @@ function mergeOverride(base, override) {
758
1054
  if (override.response !== void 0) {
759
1055
  result.response = { ...result.response, ...override.response };
760
1056
  }
1057
+ if (override.cache !== void 0) {
1058
+ result.cache = {
1059
+ ...result.cache,
1060
+ ...override.cache,
1061
+ openai: mergeRecordBlock(result.cache?.openai, override.cache.openai),
1062
+ anthropic: mergeRecordBlock(result.cache?.anthropic, override.cache.anthropic),
1063
+ gemini: mergeRecordBlock(result.cache?.gemini, override.cache.gemini),
1064
+ google: mergeRecordBlock(result.cache?.google, override.cache.google)
1065
+ };
1066
+ }
1067
+ if (override.raw !== void 0) {
1068
+ result.raw = {
1069
+ ...result.raw,
1070
+ ...override.raw,
1071
+ openai: mergeRecordBlock(result.raw?.openai, override.raw.openai),
1072
+ "openai-responses": mergeRecordBlock(result.raw?.["openai-responses"], override.raw["openai-responses"]),
1073
+ openai_responses: mergeRecordBlock(result.raw?.openai_responses, override.raw.openai_responses),
1074
+ anthropic: mergeRecordBlock(result.raw?.anthropic, override.raw.anthropic),
1075
+ gemini: mergeRecordBlock(result.raw?.gemini, override.raw.gemini),
1076
+ google: mergeRecordBlock(result.raw?.google, override.raw.google),
1077
+ openrouter: mergeRecordBlock(result.raw?.openrouter, override.raw.openrouter)
1078
+ };
1079
+ }
761
1080
  if (override.provider_options !== void 0) {
762
1081
  result.provider_options = {
763
1082
  ...result.provider_options,
@@ -769,11 +1088,19 @@ function mergeOverride(base, override) {
769
1088
  gemini: {
770
1089
  ...result.provider_options?.gemini,
771
1090
  ...override.provider_options.gemini
1091
+ },
1092
+ openrouter: {
1093
+ ...result.provider_options?.openrouter,
1094
+ ...override.provider_options.openrouter
772
1095
  }
773
1096
  };
774
1097
  }
775
1098
  return result;
776
1099
  }
1100
+ function mergeRecordBlock(base, override) {
1101
+ if (override === void 0) return base;
1102
+ return { ...base, ...override };
1103
+ }
777
1104
 
778
1105
  // src/prompt-resolution.ts
779
1106
  var DEFAULT_PROMPTS_DIR = "./prompts";
@@ -784,11 +1111,33 @@ function defaultCompiledDirForFormat(format) {
784
1111
  }
785
1112
  var sharedPromptCache = new PromptCache();
786
1113
 
1114
+ // src/cli/commands/defaults-root.ts
1115
+ import { existsSync as existsSync2 } from "fs";
1116
+ import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
1117
+ function findDefaultsRoot(filePath) {
1118
+ let current = resolve4(dirname3(filePath));
1119
+ let root = current;
1120
+ let foundDefaults = false;
1121
+ while (true) {
1122
+ if (existsSync2(join2(current, "defaults.md"))) {
1123
+ root = current;
1124
+ foundDefaults = true;
1125
+ } else if (foundDefaults) {
1126
+ return root;
1127
+ }
1128
+ const parent = dirname3(current);
1129
+ if (parent === current) {
1130
+ return root;
1131
+ }
1132
+ current = parent;
1133
+ }
1134
+ }
1135
+
787
1136
  // src/cli/commands/validate.ts
788
1137
  var HELP = `
789
1138
  promptopskit validate [sourceDir] [options]
790
1139
 
791
- Validate all prompt .md files in a directory.
1140
+ Validate prompt .md files in a directory or a single prompt file.
792
1141
 
793
1142
  Options:
794
1143
  --source, -s Source directory (default: ./prompts)
@@ -801,18 +1150,19 @@ async function validate(args) {
801
1150
  return;
802
1151
  }
803
1152
  const positional = getPositionalArgs(args, /* @__PURE__ */ new Set(["--source", "-s"]));
804
- const dir = getFlag(args, "--source", "-s") ?? positional[0] ?? DEFAULT_PROMPTS_DIR;
1153
+ const target = getFlag(args, "--source", "-s") ?? positional[0] ?? DEFAULT_PROMPTS_DIR;
805
1154
  const strict = args.includes("--strict");
806
- const files = await collectPromptFiles(dir);
1155
+ const files = await collectPromptFiles(target);
807
1156
  if (files.length === 0) {
808
- console.log(`No .md prompt files found in ${dir}`);
1157
+ console.log(`No .md prompt files found in ${target}`);
809
1158
  return;
810
1159
  }
811
1160
  let errorCount = 0;
812
1161
  let warnCount = 0;
813
1162
  for (const file of files) {
814
1163
  try {
815
- const { asset, raw } = await loadPromptFile(file, { defaultsRoot: dir });
1164
+ const defaultsRoot = files.length === 1 && file === target ? findDefaultsRoot(file) : target;
1165
+ const { asset, raw } = await loadPromptFile(file, { defaultsRoot });
816
1166
  const result = await validateAssetWithIncludes(asset, file, Object.keys(raw.frontMatter));
817
1167
  if (result.errors.length > 0) {
818
1168
  errorCount += result.errors.length;
@@ -867,12 +1217,19 @@ function getFlag(args, ...flags) {
867
1217
  }
868
1218
  return void 0;
869
1219
  }
870
- async function collectPromptFiles(dir) {
1220
+ async function collectPromptFiles(target) {
1221
+ const targetStats = await stat(target);
1222
+ if (targetStats.isFile()) {
1223
+ if (extname(target) !== ".md" || target.endsWith("defaults.md") || target.endsWith(".test.md")) {
1224
+ return [];
1225
+ }
1226
+ return [target];
1227
+ }
871
1228
  const results = [];
872
- const entries = await readdir(dir, { withFileTypes: true, recursive: true });
1229
+ const entries = await readdir(target, { withFileTypes: true, recursive: true });
873
1230
  for (const entry of entries) {
874
1231
  if (entry.isFile() && extname(entry.name) === ".md" && !entry.name.endsWith(".test.md") && entry.name !== "defaults.md") {
875
- results.push(join2(entry.parentPath ?? dir, entry.name));
1232
+ results.push(join3(entry.parentPath ?? target, entry.name));
876
1233
  }
877
1234
  }
878
1235
  return results.sort();
@@ -880,7 +1237,7 @@ async function collectPromptFiles(dir) {
880
1237
 
881
1238
  // src/cli/commands/compile.ts
882
1239
  import { readdir as readdir2, writeFile, mkdir, rm } from "fs/promises";
883
- import { join as join3, extname as extname2, relative, dirname as dirname3 } from "path";
1240
+ import { join as join4, extname as extname2, relative, dirname as dirname5 } from "path";
884
1241
  var HELP2 = `
885
1242
  promptopskit compile [sourceDir] [outputDir] [options]
886
1243
 
@@ -922,7 +1279,7 @@ async function compile(args) {
922
1279
  for (const file of files) {
923
1280
  const rel = relative(sourceDir, file).replace(/\.md$/, "");
924
1281
  const outExt = format === "esm" ? ".mjs" : ".json";
925
- const outPath = join3(outputDir, rel + outExt);
1282
+ const outPath = join4(outputDir, rel + outExt);
926
1283
  try {
927
1284
  const { asset: parsed, raw } = await loadPromptFile(file, { defaultsRoot: sourceDir });
928
1285
  const validation = await validateAssetWithIncludes(parsed, file, Object.keys(raw.frontMatter));
@@ -933,7 +1290,7 @@ async function compile(args) {
933
1290
  if (dryRun) {
934
1291
  console.log(` Would create: ${outPath}`);
935
1292
  } else {
936
- await mkdir(dirname3(outPath), { recursive: true });
1293
+ await mkdir(dirname5(outPath), { recursive: true });
937
1294
  if (format === "esm") {
938
1295
  const esmContent = `export default ${JSON.stringify(asset, null, 2)};
939
1296
  `;
@@ -990,7 +1347,7 @@ async function collectPromptFiles2(dir) {
990
1347
  const entries = await readdir2(dir, { withFileTypes: true, recursive: true });
991
1348
  for (const entry of entries) {
992
1349
  if (entry.isFile() && extname2(entry.name) === ".md" && !entry.name.endsWith(".test.md") && entry.name !== "defaults.md") {
993
- results.push(join3(entry.parentPath ?? dir, entry.name));
1350
+ results.push(join4(entry.parentPath ?? dir, entry.name));
994
1351
  }
995
1352
  }
996
1353
  return results.sort();
@@ -999,30 +1356,6 @@ async function collectPromptFiles2(dir) {
999
1356
  // src/cli/commands/render.ts
1000
1357
  import { readFile as readFile4 } from "fs/promises";
1001
1358
  import { existsSync as existsSync3 } from "fs";
1002
-
1003
- // src/cli/commands/defaults-root.ts
1004
- import { existsSync as existsSync2 } from "fs";
1005
- import { dirname as dirname4, join as join4, resolve as resolve4 } from "path";
1006
- function findDefaultsRoot(filePath) {
1007
- let current = resolve4(dirname4(filePath));
1008
- let root = current;
1009
- let foundDefaults = false;
1010
- while (true) {
1011
- if (existsSync2(join4(current, "defaults.md"))) {
1012
- root = current;
1013
- foundDefaults = true;
1014
- } else if (foundDefaults) {
1015
- return root;
1016
- }
1017
- const parent = dirname4(current);
1018
- if (parent === current) {
1019
- return root;
1020
- }
1021
- current = parent;
1022
- }
1023
- }
1024
-
1025
- // src/cli/commands/render.ts
1026
1359
  var HELP3 = `
1027
1360
  promptopskit render <file> [options]
1028
1361
 
@@ -1157,7 +1490,7 @@ async function inspect(args) {
1157
1490
 
1158
1491
  // src/cli/commands/init.ts
1159
1492
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1160
- import { join as join5, dirname as dirname5 } from "path";
1493
+ import { join as join5, dirname as dirname6, relative as relative2 } from "path";
1161
1494
  import { existsSync as existsSync4, readFileSync } from "fs";
1162
1495
  var HELP5 = `
1163
1496
  promptopskit init [dir]
@@ -1229,10 +1562,14 @@ var TEST_SIDECAR = `cases:
1229
1562
  variables:
1230
1563
  name: "World"
1231
1564
  app_context: "Welcome screen"
1565
+ response:
1566
+ message: "Hello, World! How can I help you today?"
1232
1567
  - name: named-greeting
1233
1568
  variables:
1234
1569
  name: "Alice"
1235
1570
  app_context: "Settings page"
1571
+ response:
1572
+ message: "Hello, Alice! How can I help you today?"
1236
1573
  `;
1237
1574
  var EXAMPLE_USAGE = `// Example: render the hello prompt and send it to OpenAI
1238
1575
  // Full docs: https://promptopskit.com/docs/index.html#/
@@ -1304,12 +1641,16 @@ async function init(args) {
1304
1641
  return;
1305
1642
  }
1306
1643
  const dir = args.find((a) => !a.startsWith("--")) ?? "./prompts";
1644
+ const testFilePath = join5(dirname6(dir), "tests", "hello.prompt.test.mjs");
1645
+ const promptsDirFromTest = relative2(dirname6(testFilePath), dir) || ".";
1646
+ const helloPromptTest = createHelloPromptTest(promptsDirFromTest);
1307
1647
  const files = [
1308
1648
  { path: join5(dir, "defaults.md"), content: DEFAULTS },
1309
1649
  { path: join5(dir, "hello.md"), content: HELLO_PROMPT },
1310
1650
  { path: join5(dir, "hello.test.yaml"), content: TEST_SIDECAR },
1311
1651
  { path: join5(dir, "shared", "tone.md"), content: TONE_INCLUDE },
1312
- { path: join5(dir, "example-usage.ts"), content: EXAMPLE_USAGE }
1652
+ { path: join5(dir, "example-usage.ts"), content: EXAMPLE_USAGE },
1653
+ { path: testFilePath, content: helloPromptTest }
1313
1654
  ];
1314
1655
  let created = 0;
1315
1656
  let skipped = 0;
@@ -1319,7 +1660,7 @@ async function init(args) {
1319
1660
  skipped++;
1320
1661
  continue;
1321
1662
  }
1322
- await mkdir2(dirname5(file.path), { recursive: true });
1663
+ await mkdir2(dirname6(file.path), { recursive: true });
1323
1664
  await writeFile2(file.path, file.content, "utf-8");
1324
1665
  console.log(` \u2713 ${file.path}`);
1325
1666
  created++;
@@ -1334,14 +1675,61 @@ async function init(args) {
1334
1675
  console.log(`Tip: Add to your package.json scripts:`);
1335
1676
  console.log(` "build:prompts": "promptopskit compile ${dir}"`);
1336
1677
  }
1678
+ if (!pkg.scripts?.test) {
1679
+ console.log();
1680
+ console.log(`Tip: Add a test script to run the generated prompt test:`);
1681
+ console.log(` "test": "node --test tests/*.test.mjs"`);
1682
+ }
1337
1683
  } catch {
1338
1684
  }
1339
1685
  }
1340
1686
  }
1687
+ function createHelloPromptTest(promptsDirFromTest) {
1688
+ return `import assert from 'node:assert/strict';
1689
+ import { dirname, resolve } from 'node:path';
1690
+ import test from 'node:test';
1691
+ import { fileURLToPath } from 'node:url';
1692
+ import { createPromptOpsKit } from 'promptopskit';
1693
+ import {
1694
+ getHardcodedPromptResponse,
1695
+ loadPromptTestSidecar,
1696
+ renderPromptTestCase,
1697
+ } from 'promptopskit/testing';
1698
+
1699
+ const testDir = dirname(fileURLToPath(import.meta.url));
1700
+ const promptsDir = resolve(testDir, ${JSON.stringify(promptsDirFromTest)});
1701
+
1702
+ test('hello prompt renders every sidecar case', async () => {
1703
+ const kit = createPromptOpsKit({ sourceDir: promptsDir, cache: false });
1704
+ const sidecar = await loadPromptTestSidecar(resolve(promptsDir, 'hello.test.yaml'));
1705
+
1706
+ for (const testCase of sidecar.cases) {
1707
+ const { rendered } = await renderPromptTestCase(kit, {
1708
+ sidecar,
1709
+ caseName: testCase.name,
1710
+ path: 'hello',
1711
+ provider: 'openai',
1712
+ environment: 'dev',
1713
+ strict: true,
1714
+ });
1715
+
1716
+ assert.ok(rendered.request?.body?.messages);
1717
+ }
1718
+ });
1719
+
1720
+ test('hello prompt can return a deterministic response without calling a model', async () => {
1721
+ const sidecar = await loadPromptTestSidecar(resolve(promptsDir, 'hello.test.yaml'));
1722
+
1723
+ assert.deepEqual(getHardcodedPromptResponse(sidecar, 'basic-greeting'), {
1724
+ message: 'Hello, World! How can I help you today?',
1725
+ });
1726
+ });
1727
+ `;
1728
+ }
1341
1729
 
1342
1730
  // src/cli/commands/skill.ts
1343
1731
  import { writeFile as writeFile3, readFile as readFile5, mkdir as mkdir3 } from "fs/promises";
1344
- import { dirname as dirname6 } from "path";
1732
+ import { dirname as dirname7 } from "path";
1345
1733
  import { existsSync as existsSync5 } from "fs";
1346
1734
  var MARKER_START = "<!-- promptopskit:start -->";
1347
1735
  var MARKER_END = "<!-- promptopskit:end -->";
@@ -1440,7 +1828,7 @@ async function skill(args) {
1440
1828
  written++;
1441
1829
  continue;
1442
1830
  }
1443
- await mkdir3(dirname6(filePath), { recursive: true });
1831
+ await mkdir3(dirname7(filePath), { recursive: true });
1444
1832
  await writeFile3(filePath, markedContent, "utf-8");
1445
1833
  console.log(` \u2713 ${filePath}`);
1446
1834
  written++;
@@ -1453,7 +1841,7 @@ async function skill(args) {
1453
1841
  async function deployClaude(filePath, force) {
1454
1842
  const content = CLAUDE_LINE + "\n";
1455
1843
  if (!existsSync5(filePath) || force) {
1456
- await mkdir3(dirname6(filePath), { recursive: true });
1844
+ await mkdir3(dirname7(filePath), { recursive: true });
1457
1845
  await writeFile3(filePath, content, "utf-8");
1458
1846
  console.log(` \u2713 ${filePath}`);
1459
1847
  return 1;