promptopskit 0.0.2 → 0.1.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 (41) hide show
  1. package/LICENSE +211 -211
  2. package/README.md +382 -335
  3. package/dist/{chunk-VYVEVJC3.js → chunk-B5UFGNDV.js} +2 -2
  4. package/dist/chunk-B5UFGNDV.js.map +1 -0
  5. package/dist/{chunk-5BI5FP5L.js → chunk-FSOJVXC4.js} +2 -2
  6. package/dist/chunk-FSOJVXC4.js.map +1 -0
  7. package/dist/{chunk-6KGBPHSY.js → chunk-HEFOFQ2K.js} +1 -1
  8. package/dist/chunk-HEFOFQ2K.js.map +1 -0
  9. package/dist/{chunk-NIZENC7D.js → chunk-HMYPMHTG.js} +90 -3
  10. package/dist/chunk-HMYPMHTG.js.map +1 -0
  11. package/dist/{chunk-UYTUGV7Z.js → chunk-V375NQ6C.js} +2 -2
  12. package/dist/chunk-V375NQ6C.js.map +1 -0
  13. package/dist/{chunk-PU3UPUND.js → chunk-YCRWYE6N.js} +2 -2
  14. package/dist/chunk-YCRWYE6N.js.map +1 -0
  15. package/dist/cli/index.js +167 -36
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/index.cjs +100 -14
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +8 -1
  20. package/dist/index.d.ts +8 -1
  21. package/dist/index.js +7 -8
  22. package/dist/index.js.map +1 -1
  23. package/dist/providers/anthropic.cjs.map +1 -1
  24. package/dist/providers/anthropic.js +2 -2
  25. package/dist/providers/gemini.cjs.map +1 -1
  26. package/dist/providers/gemini.js +2 -2
  27. package/dist/providers/openai.cjs.map +1 -1
  28. package/dist/providers/openai.js +2 -2
  29. package/dist/providers/openrouter.cjs.map +1 -1
  30. package/dist/providers/openrouter.js +3 -3
  31. package/dist/testing.cjs +8 -0
  32. package/dist/testing.cjs.map +1 -1
  33. package/dist/testing.js +1 -1
  34. package/dist/testing.js.map +1 -1
  35. package/package.json +117 -116
  36. package/dist/chunk-5BI5FP5L.js.map +0 -1
  37. package/dist/chunk-6KGBPHSY.js.map +0 -1
  38. package/dist/chunk-NIZENC7D.js.map +0 -1
  39. package/dist/chunk-PU3UPUND.js.map +0 -1
  40. package/dist/chunk-UYTUGV7Z.js.map +0 -1
  41. package/dist/chunk-VYVEVJC3.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/commands/validate.ts
4
- import { readdir, readFile as readFile3 } from "fs/promises";
5
- import { join, extname } from "path";
4
+ import { readdir } from "fs/promises";
5
+ import { join as join2, extname } from "path";
6
6
 
7
7
  // src/parser/parser.ts
8
8
  import matter from "gray-matter";
@@ -71,6 +71,12 @@ var SectionsSchema = z.object({
71
71
  prompt_template: z.string().optional(),
72
72
  notes: z.string().optional()
73
73
  });
74
+ var PromptDefaultsSchema = z.object({
75
+ metadata: MetadataSchema.optional(),
76
+ sections: z.object({
77
+ system_instructions: z.string().optional()
78
+ }).optional()
79
+ });
74
80
  var PromptAssetSchema = z.object({
75
81
  id: z.string(),
76
82
  schema_version: z.number().int().positive().default(1),
@@ -152,6 +158,91 @@ function parsePrompt(content, filePath) {
152
158
 
153
159
  // src/parser/loader.ts
154
160
  import { readFile } from "fs/promises";
161
+ import { dirname, join, resolve } from "path";
162
+ import matter2 from "gray-matter";
163
+ var DEFAULTS_FILE_NAME = "defaults.md";
164
+ async function loadPromptFile(filePath, options = {}) {
165
+ const content = await readFile(filePath, "utf-8");
166
+ const parsed = parsePrompt(content, filePath);
167
+ const root = options.defaultsRoot ?? dirname(filePath);
168
+ const defaults = await loadDefaultsForPath(filePath, root);
169
+ const asset = applyDefaults(parsed.asset, defaults);
170
+ return {
171
+ ...parsed,
172
+ asset
173
+ };
174
+ }
175
+ async function loadDefaultsForPath(filePath, defaultsRoot) {
176
+ const directories = getDirectoriesToCheck(filePath, defaultsRoot);
177
+ let merged = {};
178
+ for (const dir of directories) {
179
+ const defaultsPath = join(dir, DEFAULTS_FILE_NAME);
180
+ try {
181
+ const defaultsContent = await readFile(defaultsPath, "utf-8");
182
+ const defaults = parseDefaults(defaultsContent);
183
+ merged = mergeDefaults(merged, defaults);
184
+ } catch (error) {
185
+ if (error.code !== "ENOENT") {
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+ return merged;
191
+ }
192
+ function getDirectoriesToCheck(filePath, defaultsRoot) {
193
+ const dirs = [];
194
+ let current = resolve(dirname(filePath));
195
+ const boundary = defaultsRoot ? resolve(defaultsRoot) : void 0;
196
+ while (true) {
197
+ dirs.unshift(current);
198
+ if (boundary && current === boundary || current === dirname(current)) {
199
+ break;
200
+ }
201
+ current = dirname(current);
202
+ }
203
+ return dirs;
204
+ }
205
+ function parseDefaults(content) {
206
+ const { data: frontMatter, content: body } = matter2(content);
207
+ const sections = extractSections(body);
208
+ return PromptDefaultsSchema.parse({
209
+ ...frontMatter,
210
+ sections: {
211
+ system_instructions: sections.system_instructions
212
+ }
213
+ });
214
+ }
215
+ function mergeDefaults(base, local) {
216
+ return {
217
+ metadata: {
218
+ ...base.metadata ?? {},
219
+ ...local.metadata ?? {}
220
+ },
221
+ sections: {
222
+ ...base.sections ?? {},
223
+ ...local.sections ?? {}
224
+ }
225
+ };
226
+ }
227
+ function applyDefaults(asset, defaults) {
228
+ const hasDefaultMetadata = defaults.metadata && Object.keys(defaults.metadata).length > 0;
229
+ const hasDefaultSystem = !!defaults.sections?.system_instructions;
230
+ if (!hasDefaultMetadata && !hasDefaultSystem) {
231
+ return asset;
232
+ }
233
+ const mergedMetadata = {
234
+ ...defaults.metadata ?? {},
235
+ ...asset.metadata ?? {}
236
+ };
237
+ const metadata = Object.keys(mergedMetadata).length > 0 ? mergedMetadata : void 0;
238
+ const systemInstructions = asset.sections?.system_instructions ?? defaults.sections?.system_instructions;
239
+ const sections = asset.sections ? { ...asset.sections, system_instructions: systemInstructions } : systemInstructions ? { system_instructions: systemInstructions } : void 0;
240
+ return {
241
+ ...asset,
242
+ metadata,
243
+ sections
244
+ };
245
+ }
155
246
 
156
247
  // src/renderer/interpolate.ts
157
248
  var VARIABLE_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
@@ -184,20 +275,20 @@ function extractVariables(template) {
184
275
 
185
276
  // src/composition/resolve-includes.ts
186
277
  import { readFile as readFile2 } from "fs/promises";
187
- import { resolve, dirname } from "path";
278
+ import { resolve as resolve2, dirname as dirname2 } from "path";
188
279
  async function resolveIncludes(asset, basePath, visited = /* @__PURE__ */ new Set()) {
189
280
  if (!asset.includes || asset.includes.length === 0) {
190
281
  return asset;
191
282
  }
192
- const baseDir = dirname(basePath);
193
- const resolvedPath = resolve(basePath);
283
+ const baseDir = dirname2(basePath);
284
+ const resolvedPath = resolve2(basePath);
194
285
  if (visited.has(resolvedPath)) {
195
286
  throw new Error(`Circular include detected: ${resolvedPath}`);
196
287
  }
197
288
  visited.add(resolvedPath);
198
289
  let mergedSystemInstructions = "";
199
290
  for (const includePath of asset.includes) {
200
- const fullPath = resolve(baseDir, includePath);
291
+ const fullPath = resolve2(baseDir, includePath);
201
292
  if (visited.has(fullPath)) {
202
293
  throw new Error(`Circular include detected: ${fullPath} (included from ${basePath})`);
203
294
  }
@@ -399,8 +490,7 @@ async function validate(args) {
399
490
  let warnCount = 0;
400
491
  for (const file of files) {
401
492
  try {
402
- const content = await readFile3(file, "utf-8");
403
- const { asset, raw } = parsePrompt(content, file);
493
+ const { asset, raw } = await loadPromptFile(file, { defaultsRoot: dir });
404
494
  const result = await validateAssetWithIncludes(asset, file, Object.keys(raw.frontMatter));
405
495
  if (result.errors.length > 0) {
406
496
  errorCount += result.errors.length;
@@ -435,16 +525,16 @@ async function collectPromptFiles(dir) {
435
525
  const results = [];
436
526
  const entries = await readdir(dir, { withFileTypes: true, recursive: true });
437
527
  for (const entry of entries) {
438
- if (entry.isFile() && extname(entry.name) === ".md" && !entry.name.endsWith(".test.md")) {
439
- results.push(join(entry.parentPath ?? dir, entry.name));
528
+ if (entry.isFile() && extname(entry.name) === ".md" && !entry.name.endsWith(".test.md") && entry.name !== "defaults.md") {
529
+ results.push(join2(entry.parentPath ?? dir, entry.name));
440
530
  }
441
531
  }
442
532
  return results.sort();
443
533
  }
444
534
 
445
535
  // src/cli/commands/compile.ts
446
- import { readdir as readdir2, readFile as readFile4, writeFile, mkdir, rm } from "fs/promises";
447
- import { join as join2, extname as extname2, relative, dirname as dirname2 } from "path";
536
+ import { readdir as readdir2, writeFile, mkdir, rm } from "fs/promises";
537
+ import { join as join3, extname as extname2, relative, dirname as dirname3 } from "path";
448
538
  var HELP2 = `
449
539
  promptopskit compile <sourceDir> <outputDir> [options]
450
540
 
@@ -489,15 +579,14 @@ async function compile(args) {
489
579
  for (const file of files) {
490
580
  const rel = relative(sourceDir, file).replace(/\.md$/, "");
491
581
  const outExt = format === "esm" ? ".mjs" : ".json";
492
- const outPath = join2(outputDir, rel + outExt);
582
+ const outPath = join3(outputDir, rel + outExt);
493
583
  try {
494
- const content = await readFile4(file, "utf-8");
495
- const { asset: parsed } = parsePrompt(content, file);
584
+ const { asset: parsed } = await loadPromptFile(file, { defaultsRoot: sourceDir });
496
585
  const asset = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
497
586
  if (dryRun) {
498
587
  console.log(` Would create: ${outPath}`);
499
588
  } else {
500
- await mkdir(dirname2(outPath), { recursive: true });
589
+ await mkdir(dirname3(outPath), { recursive: true });
501
590
  if (format === "esm") {
502
591
  const esmContent = `export default ${JSON.stringify(asset, null, 2)};
503
592
  `;
@@ -536,15 +625,15 @@ async function collectPromptFiles2(dir) {
536
625
  const results = [];
537
626
  const entries = await readdir2(dir, { withFileTypes: true, recursive: true });
538
627
  for (const entry of entries) {
539
- if (entry.isFile() && extname2(entry.name) === ".md" && !entry.name.endsWith(".test.md")) {
540
- results.push(join2(entry.parentPath ?? dir, entry.name));
628
+ if (entry.isFile() && extname2(entry.name) === ".md" && !entry.name.endsWith(".test.md") && entry.name !== "defaults.md") {
629
+ results.push(join3(entry.parentPath ?? dir, entry.name));
541
630
  }
542
631
  }
543
632
  return results.sort();
544
633
  }
545
634
 
546
635
  // src/cli/commands/render.ts
547
- import { readFile as readFile5 } from "fs/promises";
636
+ import { readFile as readFile3 } from "fs/promises";
548
637
  import { existsSync } from "fs";
549
638
 
550
639
  // src/overrides/apply-overrides.ts
@@ -607,13 +696,13 @@ async function render(args) {
607
696
  const jsonOutput = args.includes("--json");
608
697
  let variables = {};
609
698
  if (varsFile) {
610
- const varsContent = await readFile5(varsFile, "utf-8");
699
+ const varsContent = await readFile3(varsFile, "utf-8");
611
700
  variables = JSON.parse(varsContent);
612
701
  } else {
613
702
  const sidecarPath = file.replace(/\.md$/, ".test.yaml");
614
703
  if (existsSync(sidecarPath)) {
615
704
  const { default: yaml } = await import("gray-matter");
616
- const sidecarContent = await readFile5(sidecarPath, "utf-8");
705
+ const sidecarContent = await readFile3(sidecarPath, "utf-8");
617
706
  const parsed2 = yaml(`---
618
707
  ${sidecarContent}---
619
708
  `);
@@ -623,8 +712,7 @@ ${sidecarContent}---
623
712
  }
624
713
  }
625
714
  }
626
- const content = await readFile5(file, "utf-8");
627
- const { asset: parsed } = parsePrompt(content, file);
715
+ const { asset: parsed } = await loadPromptFile(file);
628
716
  const resolved = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
629
717
  const overridden = applyOverrides(resolved, {
630
718
  environment: env,
@@ -671,7 +759,6 @@ function getFlag2(args, flag) {
671
759
  }
672
760
 
673
761
  // src/cli/commands/inspect.ts
674
- import { readFile as readFile6 } from "fs/promises";
675
762
  var HELP4 = `
676
763
  promptopskit inspect <file>
677
764
 
@@ -690,15 +777,14 @@ async function inspect(args) {
690
777
  console.error("Error: Please provide a prompt file to inspect.");
691
778
  process.exit(1);
692
779
  }
693
- const content = await readFile6(file, "utf-8");
694
- const { asset: parsed } = parsePrompt(content, file);
780
+ const { asset: parsed } = await loadPromptFile(file);
695
781
  const asset = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
696
782
  console.log(JSON.stringify(asset, null, 2));
697
783
  }
698
784
 
699
785
  // src/cli/commands/init.ts
700
786
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
701
- import { join as join3, dirname as dirname3 } from "path";
787
+ import { join as join4, dirname as dirname4 } from "path";
702
788
  import { existsSync as existsSync2, readFileSync } from "fs";
703
789
  var HELP5 = `
704
790
  promptopskit init [dir]
@@ -716,6 +802,7 @@ model: gpt-5.4
716
802
  context:
717
803
  inputs:
718
804
  - name
805
+ - app_context
719
806
  includes:
720
807
  - ./shared/tone.md
721
808
  ---
@@ -723,6 +810,7 @@ includes:
723
810
  # System instructions
724
811
 
725
812
  You are a friendly assistant. Be helpful and concise.
813
+ Current app context: {{ app_context }}.
726
814
 
727
815
  # Prompt template
728
816
 
@@ -737,13 +825,25 @@ schema_version: 1
737
825
 
738
826
  Always be polite, professional, and concise. Avoid jargon unless the user uses it first.
739
827
  `.trimStart();
828
+ var DEFAULTS = `---
829
+ metadata:
830
+ owner: my-team
831
+ review_required: true
832
+ ---
833
+
834
+ # System instructions
835
+
836
+ You are a helpful AI assistant. Follow company guidelines at all times.
837
+ `.trimStart();
740
838
  var TEST_SIDECAR = `cases:
741
839
  - name: basic-greeting
742
840
  variables:
743
841
  name: "World"
842
+ app_context: "Welcome screen"
744
843
  - name: named-greeting
745
844
  variables:
746
845
  name: "Alice"
846
+ app_context: "Settings page"
747
847
  `;
748
848
  async function init(args) {
749
849
  if (args.includes("--help") || args.includes("-h")) {
@@ -752,9 +852,10 @@ async function init(args) {
752
852
  }
753
853
  const dir = args.find((a) => !a.startsWith("--")) ?? "./prompts";
754
854
  const files = [
755
- { path: join3(dir, "hello.md"), content: HELLO_PROMPT },
756
- { path: join3(dir, "hello.test.yaml"), content: TEST_SIDECAR },
757
- { path: join3(dir, "shared", "tone.md"), content: TONE_INCLUDE }
855
+ { path: join4(dir, "defaults.md"), content: DEFAULTS },
856
+ { path: join4(dir, "hello.md"), content: HELLO_PROMPT },
857
+ { path: join4(dir, "hello.test.yaml"), content: TEST_SIDECAR },
858
+ { path: join4(dir, "shared", "tone.md"), content: TONE_INCLUDE }
758
859
  ];
759
860
  let created = 0;
760
861
  let skipped = 0;
@@ -764,7 +865,7 @@ async function init(args) {
764
865
  skipped++;
765
866
  continue;
766
867
  }
767
- await mkdir2(dirname3(file.path), { recursive: true });
868
+ await mkdir2(dirname4(file.path), { recursive: true });
768
869
  await writeFile2(file.path, file.content, "utf-8");
769
870
  console.log(` \u2713 ${file.path}`);
770
871
  created++;
@@ -786,7 +887,7 @@ async function init(args) {
786
887
 
787
888
  // src/cli/commands/skill.ts
788
889
  import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
789
- import { dirname as dirname4 } from "path";
890
+ import { dirname as dirname5 } from "path";
790
891
  import { existsSync as existsSync3 } from "fs";
791
892
  var HELP6 = `
792
893
  promptopskit skill [--target <target>] [--force]
@@ -851,7 +952,7 @@ async function skill(args) {
851
952
  return;
852
953
  }
853
954
  const content = config.wrap(SKILL_CONTENT);
854
- await mkdir3(dirname4(filePath), { recursive: true });
955
+ await mkdir3(dirname5(filePath), { recursive: true });
855
956
  await writeFile3(filePath, content, "utf-8");
856
957
  console.log(` \u2713 ${filePath}`);
857
958
  console.log();
@@ -963,6 +1064,35 @@ Included files are parsed and their \`system_instructions\` are **prepended**
963
1064
  before the including file's own system instructions. Includes resolve
964
1065
  recursively. Circular includes are detected and rejected.
965
1066
 
1067
+ > **Note:** Included files do not inherit folder defaults. Only the top-level
1068
+ > prompt that is loaded via \`loadPromptFile\` receives defaults.
1069
+
1070
+ ---
1071
+
1072
+ ## Folder defaults (\`defaults.md\`)
1073
+
1074
+ Define shared defaults for a prompt tree by adding a \`defaults.md\` file in any
1075
+ folder:
1076
+
1077
+ \`\`\`text
1078
+ prompts/
1079
+ \u251C\u2500\u2500 defaults.md # global metadata + system instructions
1080
+ \u2514\u2500\u2500 support/
1081
+ \u251C\u2500\u2500 defaults.md # overrides for support/*
1082
+ \u2514\u2500\u2500 reply.md # inherits from support/defaults.md
1083
+ \`\`\`
1084
+
1085
+ Supported default fields:
1086
+ - \`metadata\` (front matter) \u2014 merged with prompt-local metadata
1087
+ - \`# System instructions\` (body section) \u2014 used when the prompt has none
1088
+
1089
+ Rules:
1090
+ - Nearest subfolder \`defaults.md\` overrides parent defaults
1091
+ - Prompt-local values always take precedence over defaults
1092
+ - \`defaults.md\` files are skipped during compilation and validation
1093
+ - \`loadPromptFile\` defaults the search boundary to the file's own directory;
1094
+ pass \`defaultsRoot\` to enable ancestor traversal
1095
+
966
1096
  ---
967
1097
 
968
1098
  ## Environment & tier overrides
@@ -1125,7 +1255,7 @@ Hello {{ name }}
1125
1255
 
1126
1256
  | Command | Description |
1127
1257
  |---------|-------------|
1128
- | \`promptopskit init [dir]\` | Scaffold a prompts directory with starter files |
1258
+ | \`promptopskit init [dir]\` | Scaffold a prompts directory with starter files (including \`defaults.md\`) |
1129
1259
  | \`promptopskit validate <dir>\` | Validate all prompt files in a directory |
1130
1260
  | \`promptopskit compile <src> <out>\` | Compile .md prompts to JSON artifacts |
1131
1261
  | \`promptopskit render <file> [--set key=value]\` | Render a prompt preview |
@@ -1143,8 +1273,9 @@ Hello {{ name }}
1143
1273
  6. **Use environment overrides** for dev/staging/prod model differences
1144
1274
  7. **Add test sidecars** (\`.test.yaml\`) for critical prompts
1145
1275
  8. **Run \`promptopskit validate\`** before committing changes
1146
- 9. **Variable names** should be \`snake_case\`
1147
- 10. **Prompt file names** should be \`kebab-case.md\`
1276
+ 9. **Use \`defaults.md\`** to share metadata and system instructions across a folder
1277
+ 10. **Variable names** should be \`snake_case\`
1278
+ 11. **Prompt file names** should be \`kebab-case.md\`
1148
1279
  `.trimEnd();
1149
1280
 
1150
1281
  // src/cli/index.ts