promptopskit 0.0.1 → 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 (38) hide show
  1. package/README.md +88 -15
  2. package/dist/{chunk-X64JII57.js → chunk-HMYPMHTG.js} +91 -4
  3. package/dist/chunk-HMYPMHTG.js.map +1 -0
  4. package/dist/cli/index.js +593 -35
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1049 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +191 -0
  9. package/dist/index.d.ts +18 -6
  10. package/dist/index.js +28 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/anthropic.cjs +151 -0
  13. package/dist/providers/anthropic.cjs.map +1 -0
  14. package/dist/providers/anthropic.d.cts +11 -0
  15. package/dist/providers/anthropic.d.ts +2 -2
  16. package/dist/providers/gemini.cjs +155 -0
  17. package/dist/providers/gemini.cjs.map +1 -0
  18. package/dist/providers/gemini.d.cts +11 -0
  19. package/dist/providers/gemini.d.ts +2 -2
  20. package/dist/providers/openai.cjs +146 -0
  21. package/dist/providers/openai.cjs.map +1 -0
  22. package/dist/providers/openai.d.cts +11 -0
  23. package/dist/providers/openai.d.ts +2 -2
  24. package/dist/providers/openrouter.cjs +161 -0
  25. package/dist/providers/openrouter.cjs.map +1 -0
  26. package/dist/providers/openrouter.d.cts +13 -0
  27. package/dist/providers/openrouter.d.ts +2 -2
  28. package/dist/{schema-DHRI5Mzl.d.ts → schema-C6smABrt.d.cts} +3 -3
  29. package/dist/schema-C6smABrt.d.ts +695 -0
  30. package/dist/testing.cjs +236 -0
  31. package/dist/testing.cjs.map +1 -0
  32. package/dist/testing.d.cts +17 -0
  33. package/dist/testing.d.ts +1 -1
  34. package/dist/testing.js +1 -1
  35. package/dist/{types-D9hquVja.d.ts → types-D7lW5IYT.d.ts} +1 -1
  36. package/dist/types-S_c-ZEfK.d.cts +40 -0
  37. package/package.json +54 -16
  38. package/dist/chunk-X64JII57.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 readFile2 } 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,11 +71,17 @@ 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),
77
83
  description: z.string().optional(),
78
- provider: z.enum(["openai", "anthropic", "google", "openrouter", "any"]).optional(),
84
+ provider: z.enum(["openai", "anthropic", "google", "gemini", "openrouter", "any"]).optional(),
79
85
  model: z.string().optional(),
80
86
  fallback_models: z.array(z.string()).optional(),
81
87
  reasoning: ReasoningSchema.optional(),
@@ -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;
@@ -182,6 +273,45 @@ function extractVariables(template) {
182
273
  return [...vars];
183
274
  }
184
275
 
276
+ // src/composition/resolve-includes.ts
277
+ import { readFile as readFile2 } from "fs/promises";
278
+ import { resolve as resolve2, dirname as dirname2 } from "path";
279
+ async function resolveIncludes(asset, basePath, visited = /* @__PURE__ */ new Set()) {
280
+ if (!asset.includes || asset.includes.length === 0) {
281
+ return asset;
282
+ }
283
+ const baseDir = dirname2(basePath);
284
+ const resolvedPath = resolve2(basePath);
285
+ if (visited.has(resolvedPath)) {
286
+ throw new Error(`Circular include detected: ${resolvedPath}`);
287
+ }
288
+ visited.add(resolvedPath);
289
+ let mergedSystemInstructions = "";
290
+ for (const includePath of asset.includes) {
291
+ const fullPath = resolve2(baseDir, includePath);
292
+ if (visited.has(fullPath)) {
293
+ throw new Error(`Circular include detected: ${fullPath} (included from ${basePath})`);
294
+ }
295
+ const content = await readFile2(fullPath, "utf-8");
296
+ const { asset: includedAsset } = parsePrompt(content, fullPath);
297
+ const resolved = await resolveIncludes(includedAsset, fullPath, new Set(visited));
298
+ if (resolved.sections?.system_instructions) {
299
+ mergedSystemInstructions += resolved.sections.system_instructions + "\n\n";
300
+ }
301
+ }
302
+ const localSystem = asset.sections?.system_instructions ?? "";
303
+ const combinedSystem = (mergedSystemInstructions + localSystem).trim() || void 0;
304
+ return {
305
+ ...asset,
306
+ sections: {
307
+ ...asset.sections,
308
+ system_instructions: combinedSystem
309
+ },
310
+ // Drop includes from the resolved asset — they've been inlined
311
+ includes: void 0
312
+ };
313
+ }
314
+
185
315
  // src/validation/levenshtein.ts
186
316
  function levenshtein(a, b) {
187
317
  const m = a.length;
@@ -299,6 +429,24 @@ function validateAsset(asset, frontMatterKeys, filePath) {
299
429
  warnings
300
430
  };
301
431
  }
432
+ async function validateAssetWithIncludes(asset, filePath, frontMatterKeys) {
433
+ const result = validateAsset(asset, frontMatterKeys, filePath);
434
+ if (asset.includes && asset.includes.length > 0) {
435
+ try {
436
+ await resolveIncludes(asset, filePath);
437
+ } catch (err) {
438
+ const message = err instanceof Error ? err.message : String(err);
439
+ const isCircular = message.includes("Circular include");
440
+ result.errors.push({
441
+ code: isCircular ? "POK021" : "POK020",
442
+ message: isCircular ? `Circular include detected: ${message}` : `Include resolution failed: ${message}`,
443
+ filePath
444
+ });
445
+ result.valid = false;
446
+ }
447
+ }
448
+ return result;
449
+ }
302
450
  function findClosestMatch(input, candidates) {
303
451
  let best;
304
452
  let bestDist = Infinity;
@@ -342,9 +490,8 @@ async function validate(args) {
342
490
  let warnCount = 0;
343
491
  for (const file of files) {
344
492
  try {
345
- const content = await readFile2(file, "utf-8");
346
- const { asset, raw } = parsePrompt(content, file);
347
- const result = validateAsset(asset, Object.keys(raw.frontMatter), file);
493
+ const { asset, raw } = await loadPromptFile(file, { defaultsRoot: dir });
494
+ const result = await validateAssetWithIncludes(asset, file, Object.keys(raw.frontMatter));
348
495
  if (result.errors.length > 0) {
349
496
  errorCount += result.errors.length;
350
497
  console.error(` \u2717 ${file}`);
@@ -378,16 +525,16 @@ async function collectPromptFiles(dir) {
378
525
  const results = [];
379
526
  const entries = await readdir(dir, { withFileTypes: true, recursive: true });
380
527
  for (const entry of entries) {
381
- if (entry.isFile() && extname(entry.name) === ".md" && !entry.name.endsWith(".test.md")) {
382
- 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));
383
530
  }
384
531
  }
385
532
  return results.sort();
386
533
  }
387
534
 
388
535
  // src/cli/commands/compile.ts
389
- import { readdir as readdir2, readFile as readFile3, writeFile, mkdir, rm } from "fs/promises";
390
- import { join as join2, extname as extname2, relative, dirname } 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";
391
538
  var HELP2 = `
392
539
  promptopskit compile <sourceDir> <outputDir> [options]
393
540
 
@@ -432,14 +579,14 @@ async function compile(args) {
432
579
  for (const file of files) {
433
580
  const rel = relative(sourceDir, file).replace(/\.md$/, "");
434
581
  const outExt = format === "esm" ? ".mjs" : ".json";
435
- const outPath = join2(outputDir, rel + outExt);
582
+ const outPath = join3(outputDir, rel + outExt);
436
583
  try {
437
- const content = await readFile3(file, "utf-8");
438
- const { asset } = parsePrompt(content, file);
584
+ const { asset: parsed } = await loadPromptFile(file, { defaultsRoot: sourceDir });
585
+ const asset = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
439
586
  if (dryRun) {
440
587
  console.log(` Would create: ${outPath}`);
441
588
  } else {
442
- await mkdir(dirname(outPath), { recursive: true });
589
+ await mkdir(dirname3(outPath), { recursive: true });
443
590
  if (format === "esm") {
444
591
  const esmContent = `export default ${JSON.stringify(asset, null, 2)};
445
592
  `;
@@ -478,15 +625,15 @@ async function collectPromptFiles2(dir) {
478
625
  const results = [];
479
626
  const entries = await readdir2(dir, { withFileTypes: true, recursive: true });
480
627
  for (const entry of entries) {
481
- if (entry.isFile() && extname2(entry.name) === ".md" && !entry.name.endsWith(".test.md")) {
482
- 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));
483
630
  }
484
631
  }
485
632
  return results.sort();
486
633
  }
487
634
 
488
635
  // src/cli/commands/render.ts
489
- import { readFile as readFile4 } from "fs/promises";
636
+ import { readFile as readFile3 } from "fs/promises";
490
637
  import { existsSync } from "fs";
491
638
 
492
639
  // src/overrides/apply-overrides.ts
@@ -549,25 +696,25 @@ async function render(args) {
549
696
  const jsonOutput = args.includes("--json");
550
697
  let variables = {};
551
698
  if (varsFile) {
552
- const varsContent = await readFile4(varsFile, "utf-8");
699
+ const varsContent = await readFile3(varsFile, "utf-8");
553
700
  variables = JSON.parse(varsContent);
554
701
  } else {
555
702
  const sidecarPath = file.replace(/\.md$/, ".test.yaml");
556
703
  if (existsSync(sidecarPath)) {
557
704
  const { default: yaml } = await import("gray-matter");
558
- const sidecarContent = await readFile4(sidecarPath, "utf-8");
559
- const parsed = yaml(`---
705
+ const sidecarContent = await readFile3(sidecarPath, "utf-8");
706
+ const parsed2 = yaml(`---
560
707
  ${sidecarContent}---
561
708
  `);
562
- const data = parsed.data;
709
+ const data = parsed2.data;
563
710
  if (data.cases?.[0]?.variables) {
564
711
  variables = data.cases[0].variables;
565
712
  }
566
713
  }
567
714
  }
568
- const content = await readFile4(file, "utf-8");
569
- const { asset } = parsePrompt(content, file);
570
- const overridden = applyOverrides(asset, {
715
+ const { asset: parsed } = await loadPromptFile(file);
716
+ const resolved = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
717
+ const overridden = applyOverrides(resolved, {
571
718
  environment: env,
572
719
  tier
573
720
  });
@@ -612,7 +759,6 @@ function getFlag2(args, flag) {
612
759
  }
613
760
 
614
761
  // src/cli/commands/inspect.ts
615
- import { readFile as readFile5 } from "fs/promises";
616
762
  var HELP4 = `
617
763
  promptopskit inspect <file>
618
764
 
@@ -631,14 +777,14 @@ async function inspect(args) {
631
777
  console.error("Error: Please provide a prompt file to inspect.");
632
778
  process.exit(1);
633
779
  }
634
- const content = await readFile5(file, "utf-8");
635
- const { asset } = parsePrompt(content, file);
780
+ const { asset: parsed } = await loadPromptFile(file);
781
+ const asset = parsed.includes && parsed.includes.length > 0 ? await resolveIncludes(parsed, file) : parsed;
636
782
  console.log(JSON.stringify(asset, null, 2));
637
783
  }
638
784
 
639
785
  // src/cli/commands/init.ts
640
786
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
641
- import { join as join3, dirname as dirname2 } from "path";
787
+ import { join as join4, dirname as dirname4 } from "path";
642
788
  import { existsSync as existsSync2, readFileSync } from "fs";
643
789
  var HELP5 = `
644
790
  promptopskit init [dir]
@@ -656,6 +802,7 @@ model: gpt-5.4
656
802
  context:
657
803
  inputs:
658
804
  - name
805
+ - app_context
659
806
  includes:
660
807
  - ./shared/tone.md
661
808
  ---
@@ -663,6 +810,7 @@ includes:
663
810
  # System instructions
664
811
 
665
812
  You are a friendly assistant. Be helpful and concise.
813
+ Current app context: {{ app_context }}.
666
814
 
667
815
  # Prompt template
668
816
 
@@ -677,13 +825,25 @@ schema_version: 1
677
825
 
678
826
  Always be polite, professional, and concise. Avoid jargon unless the user uses it first.
679
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();
680
838
  var TEST_SIDECAR = `cases:
681
839
  - name: basic-greeting
682
840
  variables:
683
841
  name: "World"
842
+ app_context: "Welcome screen"
684
843
  - name: named-greeting
685
844
  variables:
686
845
  name: "Alice"
846
+ app_context: "Settings page"
687
847
  `;
688
848
  async function init(args) {
689
849
  if (args.includes("--help") || args.includes("-h")) {
@@ -692,9 +852,10 @@ async function init(args) {
692
852
  }
693
853
  const dir = args.find((a) => !a.startsWith("--")) ?? "./prompts";
694
854
  const files = [
695
- { path: join3(dir, "hello.md"), content: HELLO_PROMPT },
696
- { path: join3(dir, "hello.test.yaml"), content: TEST_SIDECAR },
697
- { 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 }
698
859
  ];
699
860
  let created = 0;
700
861
  let skipped = 0;
@@ -704,7 +865,7 @@ async function init(args) {
704
865
  skipped++;
705
866
  continue;
706
867
  }
707
- await mkdir2(dirname2(file.path), { recursive: true });
868
+ await mkdir2(dirname4(file.path), { recursive: true });
708
869
  await writeFile2(file.path, file.content, "utf-8");
709
870
  console.log(` \u2713 ${file.path}`);
710
871
  created++;
@@ -724,8 +885,401 @@ async function init(args) {
724
885
  }
725
886
  }
726
887
 
727
- // src/cli/index.ts
888
+ // src/cli/commands/skill.ts
889
+ import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
890
+ import { dirname as dirname5 } from "path";
891
+ import { existsSync as existsSync3 } from "fs";
728
892
  var HELP6 = `
893
+ promptopskit skill [--target <target>] [--force]
894
+
895
+ Deploy AI agent instructions so coding assistants know how to
896
+ create and manage prompts using promptopskit.
897
+
898
+ Targets:
899
+ copilot (default) .github/instructions/promptopskit.instructions.md
900
+ cursor .cursor/rules/promptopskit.mdc
901
+ generic .ai/promptopskit-skill.md
902
+
903
+ Options:
904
+ --target, -t Target AI tool (copilot, cursor, generic)
905
+ --force, -f Overwrite existing file
906
+ --help, -h Show this help
907
+ `.trim();
908
+ var TARGETS = {
909
+ copilot: {
910
+ path: ".github/instructions/promptopskit.instructions.md",
911
+ wrap: (content) => `---
912
+ applyTo: "**"
913
+ ---
914
+
915
+ ${content}`
916
+ },
917
+ cursor: {
918
+ path: ".cursor/rules/promptopskit.mdc",
919
+ wrap: (content) => `---
920
+ description: How to create and manage prompts using promptopskit
921
+ globs: "**"
922
+ alwaysApply: true
923
+ ---
924
+
925
+ ${content}`
926
+ },
927
+ generic: {
928
+ path: ".ai/promptopskit-skill.md",
929
+ wrap: (content) => content
930
+ }
931
+ };
932
+ async function skill(args) {
933
+ if (args.includes("--help") || args.includes("-h")) {
934
+ console.log(HELP6);
935
+ return;
936
+ }
937
+ const force = args.includes("--force") || args.includes("-f");
938
+ let target = "copilot";
939
+ const targetIdx = args.findIndex((a) => a === "--target" || a === "-t");
940
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
941
+ target = args[targetIdx + 1];
942
+ }
943
+ const config = TARGETS[target];
944
+ if (!config) {
945
+ console.error(`Unknown target: ${target}`);
946
+ console.error(`Valid targets: ${Object.keys(TARGETS).join(", ")}`);
947
+ process.exit(1);
948
+ }
949
+ const filePath = config.path;
950
+ if (existsSync3(filePath) && !force) {
951
+ console.log(` skip ${filePath} (already exists, use --force to overwrite)`);
952
+ return;
953
+ }
954
+ const content = config.wrap(SKILL_CONTENT);
955
+ await mkdir3(dirname5(filePath), { recursive: true });
956
+ await writeFile3(filePath, content, "utf-8");
957
+ console.log(` \u2713 ${filePath}`);
958
+ console.log();
959
+ console.log(`AI agents will now understand how to create and manage prompts with promptopskit.`);
960
+ }
961
+ var SKILL_CONTENT = `# promptopskit \u2014 Prompt Engineering Skill
962
+
963
+ This project uses **promptopskit** to manage LLM prompts as code.
964
+ Prompts live in markdown files with YAML front matter, are validated against
965
+ a schema, and render into provider-specific request bodies (OpenAI, Anthropic,
966
+ Gemini, OpenRouter). Follow these instructions when creating or editing prompts.
967
+
968
+ ---
969
+
970
+ ## Prompt file format
971
+
972
+ Every prompt is a \`.md\` file with two parts:
973
+
974
+ 1. **YAML front matter** \u2014 model settings, provider config, variables, overrides
975
+ 2. **Markdown body** \u2014 sections separated by H1 headings
976
+
977
+ ### Minimal example
978
+
979
+ \`\`\`markdown
980
+ ---
981
+ id: greeting
982
+ schema_version: 1
983
+ provider: openai
984
+ model: gpt-5.4
985
+ context:
986
+ inputs:
987
+ - name
988
+ ---
989
+
990
+ # System instructions
991
+
992
+ You are a helpful assistant.
993
+
994
+ # Prompt template
995
+
996
+ Hello {{ name }}, how can I help you?
997
+ \`\`\`
998
+
999
+ ---
1000
+
1001
+ ## Front matter reference
1002
+
1003
+ | Field | Type | Required | Description |
1004
+ |-------|------|----------|-------------|
1005
+ | \`id\` | string | **yes** | Unique identifier for the prompt |
1006
+ | \`schema_version\` | number | yes | Always \`1\` |
1007
+ | \`description\` | string | no | Human-readable description |
1008
+ | \`provider\` | enum | no | \`openai\`, \`anthropic\`, \`google\`, \`gemini\`, \`openrouter\`, or \`any\` |
1009
+ | \`model\` | string | no | Model identifier (e.g. \`gpt-5.4\`, \`claude-sonnet-4-20250514\`) |
1010
+ | \`fallback_models\` | string[] | no | Ordered fallback model list |
1011
+ | \`reasoning\` | object | no | \`{ effort: low|medium|high, budget_tokens: number }\` |
1012
+ | \`sampling\` | object | no | \`{ temperature, top_p, frequency_penalty, presence_penalty, stop, max_output_tokens }\` |
1013
+ | \`response\` | object | no | \`{ format: text|json|markdown, stream: boolean }\` |
1014
+ | \`tools\` | array | no | Tool names (strings) or inline definitions with \`{ name, description, input_schema }\` |
1015
+ | \`mcp\` | object | no | \`{ servers: [string | { name, config }] }\` |
1016
+ | \`context.inputs\` | string[] | no | Declared variable names used in templates |
1017
+ | \`context.history\` | object | no | \`{ max_items: number }\` |
1018
+ | \`includes\` | string[] | no | Relative paths to other prompt files to include |
1019
+ | \`environments\` | object | no | Per-environment overrides (see Overrides) |
1020
+ | \`tiers\` | object | no | Per-tier overrides (see Overrides) |
1021
+ | \`metadata\` | object | no | \`{ owner, tags, review_required, stable }\` |
1022
+
1023
+ ---
1024
+
1025
+ ## Sections (markdown body)
1026
+
1027
+ Use H1 headings to define sections. The parser recognizes these headings
1028
+ (case-insensitive):
1029
+
1030
+ | Heading | Maps to | Purpose |
1031
+ |---------|---------|---------|
1032
+ | \`# System instructions\` | \`system_instructions\` | System/developer message |
1033
+ | \`# Prompt template\` | \`prompt_template\` | User message template |
1034
+ | \`# Notes\` | \`notes\` | Documentation only \u2014 not sent to the model |
1035
+
1036
+ If the body has **no H1 headings**, the entire body becomes the \`prompt_template\`.
1037
+
1038
+ ---
1039
+
1040
+ ## Variable interpolation
1041
+
1042
+ Use \`{{ variable_name }}\` syntax in system instructions and prompt template
1043
+ sections. Variables are replaced at render time.
1044
+
1045
+ Rules:
1046
+ - Declare all variables in \`context.inputs\` \u2014 validation warns on undeclared usage
1047
+ - Escape literal braces with \`\\\\{{\` and \`\\\\}}\`
1048
+ - In strict mode, missing variables throw an error
1049
+ - In permissive mode, unresolved placeholders are left intact
1050
+
1051
+ ---
1052
+
1053
+ ## Includes (composition)
1054
+
1055
+ Compose prompts from shared fragments:
1056
+
1057
+ \`\`\`yaml
1058
+ includes:
1059
+ - ./shared/tone.md
1060
+ - ./shared/safety.md
1061
+ \`\`\`
1062
+
1063
+ Included files are parsed and their \`system_instructions\` are **prepended**
1064
+ before the including file's own system instructions. Includes resolve
1065
+ recursively. Circular includes are detected and rejected.
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
+
1096
+ ---
1097
+
1098
+ ## Environment & tier overrides
1099
+
1100
+ Override model settings per environment or tier:
1101
+
1102
+ \`\`\`yaml
1103
+ environments:
1104
+ development:
1105
+ model: gpt-4.1-mini
1106
+ sampling:
1107
+ temperature: 0.9
1108
+ production:
1109
+ model: gpt-5.4
1110
+ sampling:
1111
+ temperature: 0.3
1112
+
1113
+ tiers:
1114
+ free:
1115
+ model: gpt-4.1-mini
1116
+ sampling:
1117
+ max_output_tokens: 500
1118
+ premium:
1119
+ model: gpt-5.4
1120
+ \`\`\`
1121
+
1122
+ Overridable fields: \`model\`, \`fallback_models\`, \`reasoning\`, \`sampling\`,
1123
+ \`response\`, \`tools\`.
1124
+
1125
+ Override application order: **base \u2192 environment \u2192 tier \u2192 runtime**.
1126
+
1127
+ ---
1128
+
1129
+ ## Test sidecars
1130
+
1131
+ Create a \`.test.yaml\` file alongside a prompt to define test cases:
1132
+
1133
+ \`\`\`yaml
1134
+ # greeting.test.yaml
1135
+ cases:
1136
+ - name: basic
1137
+ variables:
1138
+ name: "World"
1139
+ - name: formal
1140
+ variables:
1141
+ name: "Dr. Smith"
1142
+ \`\`\`
1143
+
1144
+ ---
1145
+
1146
+ ## Using the library (TypeScript / JavaScript)
1147
+
1148
+ ### Quick start
1149
+
1150
+ \`\`\`typescript
1151
+ import { createPromptOpsKit } from 'promptopskit';
1152
+
1153
+ const kit = createPromptOpsKit({ sourceDir: './prompts' });
1154
+
1155
+ // Load \u2192 resolve includes \u2192 apply overrides \u2192 render
1156
+ const request = await kit.renderPrompt('greeting', {
1157
+ variables: { name: 'Alice' },
1158
+ environment: 'production',
1159
+ });
1160
+
1161
+ // request.body is ready for the provider's API
1162
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
1163
+ method: 'POST',
1164
+ headers: {
1165
+ 'Content-Type': 'application/json',
1166
+ Authorization: \`Bearer \${apiKey}\`,
1167
+ },
1168
+ body: JSON.stringify(request.body),
1169
+ });
1170
+ \`\`\`
1171
+
1172
+ ### Step-by-step API
1173
+
1174
+ \`\`\`typescript
1175
+ import {
1176
+ parsePrompt,
1177
+ resolveIncludes,
1178
+ applyOverrides,
1179
+ getAdapter,
1180
+ } from 'promptopskit';
1181
+ import { readFileSync } from 'fs';
1182
+
1183
+ // 1. Parse a prompt file
1184
+ const source = readFileSync('./prompts/greeting.md', 'utf-8');
1185
+ const asset = parsePrompt(source, 'greeting.md');
1186
+
1187
+ // 2. Resolve includes
1188
+ const resolved = await resolveIncludes(asset, './prompts');
1189
+
1190
+ // 3. Apply overrides
1191
+ const configured = applyOverrides(resolved, {
1192
+ environment: 'production',
1193
+ tier: 'premium',
1194
+ });
1195
+
1196
+ // 4. Get provider adapter and render
1197
+ const adapter = getAdapter(configured.provider ?? 'openai');
1198
+ const request = adapter.render(configured, {
1199
+ variables: { name: 'Alice' },
1200
+ history: [
1201
+ { role: 'user', content: 'Previous message' },
1202
+ { role: 'assistant', content: 'Previous response' },
1203
+ ],
1204
+ });
1205
+ \`\`\`
1206
+
1207
+ ### Available provider adapters
1208
+
1209
+ | Provider | Import path | Provider request format |
1210
+ |----------|------------|----------------------|
1211
+ | OpenAI | \`promptopskit\` or \`promptopskit/openai\` | Chat Completions API |
1212
+ | Anthropic | \`promptopskit/anthropic\` | Messages API |
1213
+ | Gemini | \`promptopskit/gemini\` | GenerateContent API |
1214
+ | OpenRouter | \`promptopskit/openrouter\` | OpenAI-compatible + extras |
1215
+
1216
+ ### Validation
1217
+
1218
+ \`\`\`typescript
1219
+ import { validateAsset, parsePrompt } from 'promptopskit';
1220
+
1221
+ const asset = parsePrompt(source);
1222
+ const result = validateAsset(asset);
1223
+
1224
+ if (!result.valid) {
1225
+ console.error(result.errors); // Error codes: POK001-POK021
1226
+ }
1227
+ \`\`\`
1228
+
1229
+ ### Testing helpers
1230
+
1231
+ \`\`\`typescript
1232
+ import { createMockAsset, parseTestPrompt } from 'promptopskit/testing';
1233
+
1234
+ // Create a mock asset for unit tests
1235
+ const mock = createMockAsset({ model: 'gpt-4.1-mini' });
1236
+
1237
+ // Parse an inline prompt string for tests
1238
+ const asset = parseTestPrompt(\`
1239
+ ---
1240
+ id: test
1241
+ schema_version: 1
1242
+ provider: openai
1243
+ model: gpt-5.4
1244
+ ---
1245
+
1246
+ # Prompt template
1247
+
1248
+ Hello {{ name }}
1249
+ \`);
1250
+ \`\`\`
1251
+
1252
+ ---
1253
+
1254
+ ## CLI commands
1255
+
1256
+ | Command | Description |
1257
+ |---------|-------------|
1258
+ | \`promptopskit init [dir]\` | Scaffold a prompts directory with starter files (including \`defaults.md\`) |
1259
+ | \`promptopskit validate <dir>\` | Validate all prompt files in a directory |
1260
+ | \`promptopskit compile <src> <out>\` | Compile .md prompts to JSON artifacts |
1261
+ | \`promptopskit render <file> [--set key=value]\` | Render a prompt preview |
1262
+ | \`promptopskit inspect <file>\` | Print the normalized prompt asset |
1263
+
1264
+ ---
1265
+
1266
+ ## Conventions to follow
1267
+
1268
+ 1. **One prompt per file** \u2014 each \`.md\` file is a single prompt asset
1269
+ 2. **Always set \`id\` and \`schema_version: 1\`** in front matter
1270
+ 3. **Declare all variables** in \`context.inputs\` that appear in templates
1271
+ 4. **Use includes** for shared system instructions (tone, safety, formatting)
1272
+ 5. **Keep prompt templates focused** \u2014 compose behavior via includes, not duplication
1273
+ 6. **Use environment overrides** for dev/staging/prod model differences
1274
+ 7. **Add test sidecars** (\`.test.yaml\`) for critical prompts
1275
+ 8. **Run \`promptopskit validate\`** before committing changes
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\`
1279
+ `.trimEnd();
1280
+
1281
+ // src/cli/index.ts
1282
+ var HELP7 = `
729
1283
  promptopskit \u2014 Manage prompts, system instructions, tools, and model settings as code
730
1284
 
731
1285
  Usage:
@@ -737,6 +1291,7 @@ Commands:
737
1291
  compile <src> <out> [options] Compile .md prompts to JSON/ESM artifacts
738
1292
  render <file> [options] Render a prompt preview
739
1293
  inspect <file> Print normalized prompt asset
1294
+ skill [options] Deploy AI agent instructions into your project
740
1295
 
741
1296
  Options:
742
1297
  --help, -h Show this help message
@@ -748,7 +1303,7 @@ async function main() {
748
1303
  const args = process.argv.slice(2);
749
1304
  const command = args[0];
750
1305
  if (!command || command === "--help" || command === "-h") {
751
- console.log(HELP6);
1306
+ console.log(HELP7);
752
1307
  process.exit(0);
753
1308
  }
754
1309
  if (command === "--version" || command === "-v") {
@@ -772,9 +1327,12 @@ async function main() {
772
1327
  case "inspect":
773
1328
  await inspect(commandArgs);
774
1329
  break;
1330
+ case "skill":
1331
+ await skill(commandArgs);
1332
+ break;
775
1333
  default:
776
1334
  console.error(`Unknown command: ${command}`);
777
- console.log(HELP6);
1335
+ console.log(HELP7);
778
1336
  process.exit(1);
779
1337
  }
780
1338
  }