Skip to content

Commit 4c232e6

Browse files
IMPROVEMENT: Simplified renderPrompts signature by assuming default source and compiled paths based on the init and compile defaults
1 parent 8e57e45 commit 4c232e6

9 files changed

Lines changed: 162 additions & 51 deletions

File tree

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ npm install promptopskit
3434
### 1. Scaffold starter prompts
3535

3636
```bash
37-
npx promptopskit init ./prompts
37+
npx promptopskit init
3838
npx promptopskit skill
3939
```
4040

@@ -84,7 +84,7 @@ You are a helpful support assistant working in {{ app_context }}.
8484
```typescript
8585
import { createPromptOpsKit } from 'promptopskit';
8686

87-
const kit = createPromptOpsKit({ sourceDir: './prompts' });
87+
const kit = createPromptOpsKit();
8888

8989
const result = await kit.renderPrompt({
9090
path: 'support/reply',
@@ -110,7 +110,6 @@ You can control context size warning behavior at the kit level:
110110

111111
```typescript
112112
const kit = createPromptOpsKit({
113-
sourceDir: './prompts',
114113
warnings: {
115114
contextSize: process.env.NODE_ENV === 'production' ? 'off' : 'console-and-result',
116115
},
@@ -141,7 +140,7 @@ Each adapter produces a `{ body, provider, model }` object shaped for the target
141140
```typescript
142141
// OpenAI
143142
import { createPromptOpsKit } from 'promptopskit';
144-
const kit = createPromptOpsKit({ sourceDir: './prompts' });
143+
const kit = createPromptOpsKit();
145144
const { request } = await kit.renderPrompt({
146145
path: 'hello',
147146
provider: 'openai',
@@ -207,17 +206,14 @@ const request = openaiAdapter.render(prompt, {
207206

208207
In browser or client-side code, keep provider credentials on the server. Use the rendered request body with your own server endpoint, server action, or edge function rather than calling a provider directly from the client.
209208

210-
On the server, adapters also provide async prompt-aware helpers so you can pass a prompt key plus `sourceDir` and `compiledDir` without creating a `PromptOpsKit` instance:
209+
On the server, adapters also provide async prompt-aware helpers so you can use the default `./prompts` and `./.generated-prompts/json` directories without creating a `PromptOpsKit` instance:
211210

212211
```typescript
213-
import path from 'node:path';
214212
import { openaiAdapter } from 'promptopskit/openai';
215213

216214
const request = await openaiAdapter.renderPrompt(
217215
{
218216
path: 'summarizePullRequest',
219-
sourceDir: path.join(process.cwd(), 'prompts'),
220-
compiledDir: path.join(process.cwd(), '.generated-prompts', 'json'),
221217
},
222218
{
223219
environment: 'dev',
@@ -229,6 +225,8 @@ const request = await openaiAdapter.renderPrompt(
229225
);
230226
```
231227

228+
If you need a different layout, keep passing `sourceDir` and `compiledDir` explicitly.
229+
232230
`renderPrompt()` and `validatePrompt()` use the same source-versus-compiled resolution rules as `kit.renderPrompt()`. The existing synchronous `render()` and `validate()` methods still work for already-resolved compiled or inline assets.
233231

234232
## Optional UsageTap Tracking

docs/api-reference.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ Creates a `PromptOpsKit` instance.
77
```typescript
88
import { createPromptOpsKit } from 'promptopskit';
99

10+
const kit = createPromptOpsKit();
11+
```
12+
13+
| Option | Type | Default | Description |
14+
|--------|------|---------|-------------|
15+
| `sourceDir` | `string` | `./prompts` | Path to prompt `.md` files |
16+
| `compiledDir` | `string` | `./.generated-prompts/json` | Path to compiled artifacts |
17+
| `mode` | `'auto' \| 'compiled-only' \| 'source-only'` | `'auto'` | Resolution strategy |
18+
| `cache` | `boolean` | `true` | Enable LRU cache with mtime invalidation |
19+
| `warnings.contextSize` | `'auto' \| 'off' \| 'result-only' \| 'console' \| 'console-and-result'` | `'auto'` | Control whether render-time context size warnings are returned, logged, both, or suppressed |
20+
21+
Example with overrides:
22+
23+
```typescript
1024
const kit = createPromptOpsKit({
1125
sourceDir: './prompts',
1226
compiledDir: './.generated-prompts/json',
@@ -18,14 +32,6 @@ const kit = createPromptOpsKit({
1832
});
1933
```
2034

21-
| Option | Type | Default | Description |
22-
|--------|------|---------|-------------|
23-
| `sourceDir` | `string` || Path to prompt `.md` files (required) |
24-
| `compiledDir` | `string` || Path to compiled artifacts |
25-
| `mode` | `'auto' \| 'compiled-only' \| 'source-only'` | `'auto'` | Resolution strategy |
26-
| `cache` | `boolean` | `true` | Enable LRU cache with mtime invalidation |
27-
| `warnings.contextSize` | `'auto' \| 'off' \| 'result-only' \| 'console' \| 'console-and-result'` | `'auto'` | Control whether render-time context size warnings are returned, logged, both, or suppressed |
28-
2935
### Resolution modes
3036

3137
| Mode | Behavior |
@@ -247,7 +253,7 @@ const result = await renderPrompt({
247253
source: '---\nid: inline\nschema_version: 1\n---\n\nHello {{ name }}!',
248254
provider: 'openai',
249255
variables: { name: 'World' },
250-
sourceDir: './prompts', // defaults to '.'
256+
sourceDir: './prompts', // defaults to ./prompts
251257
warnings: { contextSize: 'result-only' },
252258
});
253259
```
@@ -278,6 +284,6 @@ import type {
278284

279285
Provider helper types:
280286

281-
- `ProviderPromptLookup``{ path, sourceDir, compiledDir?, mode?, cache? }` for adapter-managed source or compiled lookup
287+
- `ProviderPromptLookup``{ path, sourceDir?, compiledDir?, mode?, cache? }` for adapter-managed source or compiled lookup
282288
- `ProviderInlinePromptSource``{ source }` for adapter-managed inline prompt source
283289
- `ProviderPromptInput` — union of `ResolvedPromptAsset`, `ProviderPromptLookup`, and `ProviderInlinePromptSource`

docs/providers.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ PromptOpsKit ships four provider adapters. Each produces a `{ body, provider, mo
1616
```typescript
1717
import { createPromptOpsKit } from 'promptopskit';
1818

19-
const kit = createPromptOpsKit({ sourceDir: './prompts' });
19+
const kit = createPromptOpsKit();
2020

2121
const { request } = await kit.renderPrompt({
2222
path: 'hello',
@@ -61,14 +61,11 @@ Direct adapter rendering accepts the same `environment` and `tier` selectors as
6161
Server-side example:
6262

6363
```typescript
64-
import path from 'node:path';
6564
import { openaiAdapter } from 'promptopskit/openai';
6665

6766
const request = await openaiAdapter.renderPrompt(
6867
{
6968
path: 'summarizePullRequest',
70-
sourceDir: path.join(process.cwd(), 'prompts'),
71-
compiledDir: path.join(process.cwd(), '.generated-prompts', 'json'),
7269
},
7370
{
7471
environment: 'dev',
@@ -80,6 +77,32 @@ const request = await openaiAdapter.renderPrompt(
8077
);
8178
```
8279

80+
Pass `sourceDir` and `compiledDir` only when you want to override the default `./prompts` and `./.generated-prompts/json` locations.
81+
82+
## Choosing JSON vs ESM
83+
84+
PromptOpsKit's path-based runtime lookup reads compiled `.json` files from disk. That makes JSON the natural server default when you want to resolve prompts by key at runtime with `renderPrompt({ path })` or `createPromptOpsKit().renderPrompt({ path })`.
85+
86+
ESM is the better fit when prompts should be imported into code and bundled with the application instead of discovered from the filesystem at runtime.
87+
88+
| Format | Best when | Advantages | Tradeoffs |
89+
|--------|-----------|------------|-----------|
90+
| `json` | You want runtime lookup by prompt key on a Node server | Matches the built-in `compiledDir` lookup path, easy to regenerate, works well with the default `./.generated-prompts/json` layout | Depends on filesystem access, deployment packaging, and stable working-directory-relative paths |
91+
| `esm` | You want prompts bundled as imports | Better for bundlers, browser-safe import flows, and deployments where static imports are more reliable than runtime fs reads | Not used by the built-in path lookup flow; you import the compiled prompt and call `adapter.render()` or `adapter.validate()` directly |
92+
93+
Deployment guidance:
94+
95+
- AWS Lambda: use `json` if you ship prompt artifacts alongside the function and want runtime lookup by path; use `esm` if your Lambda is bundled and you want prompts embedded via imports.
96+
- Cloudflare Workers: prefer `esm` or inline prompt assets. Workers-style runtimes are bundle-oriented and do not match the filesystem-based `renderPrompt()` lookup model.
97+
- Vercel: prefer `esm` for Edge or heavily bundled serverless functions; `json` is fine for Node functions only when the compiled asset directory is reliably included.
98+
- Railway and container-style Node hosting: `json` is usually the simplest choice because the runtime filesystem layout is predictable.
99+
- Browser or client-only code: use `esm` imports or inline prompt assets; do not rely on `renderPrompt()` filesystem lookup.
100+
101+
Rule of thumb:
102+
103+
- Choose `json` for server-side prompt resolution by file path.
104+
- Choose `esm` for import-based rendering and bundle-oriented deployments.
105+
83106
## Browser / client-side usage
84107

85108
The top-level `promptopskit` runtime is Node-oriented. It supports prompt loading and compilation flows that import file-system/path modules, so do not use `createPromptOpsKit()` inside browser-only code or client components.

src/cli/commands/compile.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readdir, writeFile, mkdir, rm } from 'node:fs/promises';
22
import { join, extname, relative, dirname } from 'node:path';
33
import { loadPromptFile } from '../../parser/index.js';
44
import { resolveIncludes } from '../../composition/index.js';
5+
import { DEFAULT_PROMPTS_DIR, defaultCompiledDirForFormat } from '../../prompt-resolution.js';
56

67
const HELP = `
78
promptopskit compile [sourceDir] [outputDir] [options]
@@ -33,8 +34,8 @@ export async function compile(args: string[]): Promise<void> {
3334
process.exit(1);
3435
}
3536

36-
const sourceDir = getFlag(args, '--source', '-s') ?? positional[0] ?? './prompts';
37-
const outputDir = getFlag(args, '--output', '-o') ?? positional[1] ?? defaultOutputDirForFormat(format);
37+
const sourceDir = getFlag(args, '--source', '-s') ?? positional[0] ?? DEFAULT_PROMPTS_DIR;
38+
const outputDir = getFlag(args, '--output', '-o') ?? positional[1] ?? defaultCompiledDirForFormat(format);
3839

3940
// Collect prompt files
4041
const files = await collectPromptFiles(sourceDir);
@@ -98,11 +99,6 @@ export async function compile(args: string[]): Promise<void> {
9899
process.exit(1);
99100
}
100101
}
101-
102-
function defaultOutputDirForFormat(format: 'json' | 'esm'): string {
103-
return format === 'esm' ? './.generated-prompts/esm' : './.generated-prompts/json';
104-
}
105-
106102
function getPositionalArgs(args: string[], flagsWithValues: Set<string>): string[] {
107103
const positional: string[] = [];
108104

src/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { validateAsset, validateAssetWithIncludes } from './validation/index.js'
1010
import { PromptCache } from './cache.js';
1111
import { collectContextSizeWarnings } from './context.js';
1212
import {
13+
DEFAULT_PROMPTS_DIR,
1314
loadPromptAsset,
1415
resolveInlinePromptSource,
1516
resolvePromptAsset,
17+
withPromptResolutionDefaults,
1618
} from './prompt-resolution.js';
1719
import type { PromptAsset, PromptAssetOverrides, ResolvedPromptAsset } from './schema/index.js';
1820
import type { ProviderRequest, RuntimeRenderOptions } from './providers/types.js';
@@ -84,7 +86,7 @@ export {
8486
// --- Config ---
8587

8688
export interface PromptOpsKitConfig {
87-
sourceDir: string;
89+
sourceDir?: string;
8890
compiledDir?: string;
8991
mode?: 'auto' | 'compiled-only' | 'source-only';
9092
cache?: boolean;
@@ -170,10 +172,11 @@ export class PromptOpsKit {
170172
private promptCache: PromptCache<PromptAsset>;
171173

172174
constructor(config: PromptOpsKitConfig) {
175+
const resolvedConfig = withPromptResolutionDefaults(config);
173176
this.config = {
174-
...config,
175-
mode: config.mode ?? 'auto',
176-
cache: config.cache ?? true,
177+
...resolvedConfig,
178+
mode: resolvedConfig.mode ?? 'auto',
179+
cache: resolvedConfig.cache ?? true,
177180
};
178181
this.promptCache = new PromptCache();
179182
}
@@ -275,15 +278,15 @@ export class PromptOpsKit {
275278

276279
// --- Factory ---
277280

278-
export function createPromptOpsKit(config: PromptOpsKitConfig): PromptOpsKit {
281+
export function createPromptOpsKit(config: PromptOpsKitConfig = {}): PromptOpsKit {
279282
return new PromptOpsKit(config);
280283
}
281284

282285
// --- Standalone convenience ---
283286

284287
/**
285288
* Standalone renderPrompt for quick usage without creating a PromptOpsKit instance.
286-
* Requires either `source` (inline) or `path` + implicit sourceDir of '.'.
289+
* Requires either `source` (inline) or `path` + implicit sourceDir of ./prompts.
287290
*/
288291
export async function renderPrompt(
289292
options: RenderPromptOptions & {
@@ -292,7 +295,7 @@ export async function renderPrompt(
292295
},
293296
): Promise<RenderResult> {
294297
const kit = createPromptOpsKit({
295-
sourceDir: options.sourceDir ?? '.',
298+
sourceDir: options.sourceDir ?? DEFAULT_PROMPTS_DIR,
296299
cache: false,
297300
warnings: options.warnings,
298301
});

src/prompt-resolution.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,51 @@ import type { PromptAsset, PromptAssetOverrides, ResolvedPromptAsset } from './s
1010

1111
export type PromptResolutionMode = 'auto' | 'compiled-only' | 'source-only';
1212

13+
export const DEFAULT_PROMPTS_DIR = './prompts';
14+
export const DEFAULT_COMPILED_JSON_DIR = './.generated-prompts/json';
15+
export const DEFAULT_COMPILED_ESM_DIR = './.generated-prompts/esm';
16+
1317
export interface PromptResolutionConfig {
14-
sourceDir: string;
18+
sourceDir?: string;
1519
compiledDir?: string;
1620
mode?: PromptResolutionMode;
1721
cache?: boolean;
1822
}
1923

24+
export interface ResolvedPromptResolutionConfig {
25+
sourceDir: string;
26+
compiledDir: string;
27+
mode?: PromptResolutionMode;
28+
cache?: boolean;
29+
}
30+
31+
export function defaultCompiledDirForFormat(format: 'json' | 'esm'): string {
32+
return format === 'esm' ? DEFAULT_COMPILED_ESM_DIR : DEFAULT_COMPILED_JSON_DIR;
33+
}
34+
35+
export function withPromptResolutionDefaults(config: PromptResolutionConfig): ResolvedPromptResolutionConfig {
36+
return {
37+
...config,
38+
sourceDir: config.sourceDir ?? DEFAULT_PROMPTS_DIR,
39+
compiledDir: config.compiledDir ?? DEFAULT_COMPILED_JSON_DIR,
40+
};
41+
}
42+
2043
const sharedPromptCache = new PromptCache<PromptAsset>();
2144

2245
export async function loadPromptAsset(
2346
promptPath: string,
2447
config: PromptResolutionConfig,
2548
promptCache: PromptCache<PromptAsset> = sharedPromptCache,
2649
): Promise<PromptAsset> {
27-
const mode = config.mode ?? 'auto';
50+
const resolvedConfig = withPromptResolutionDefaults(config);
51+
const mode = resolvedConfig.mode ?? 'auto';
2852

29-
if (mode !== 'source-only' && config.compiledDir) {
30-
const compiledFile = resolve(config.compiledDir, promptPath + '.json');
53+
if (mode !== 'source-only' && resolvedConfig.compiledDir) {
54+
const compiledFile = resolve(resolvedConfig.compiledDir, promptPath + '.json');
3155
if (existsSync(compiledFile)) {
3256
if (mode === 'auto') {
33-
const sourceFile = resolve(config.sourceDir, promptPath + '.md');
57+
const sourceFile = resolve(resolvedConfig.sourceDir, promptPath + '.md');
3458
if (existsSync(sourceFile)) {
3559
const compiledMtime = statSync(compiledFile).mtimeMs;
3660
const sourceMtime = statSync(sourceFile).mtimeMs;
@@ -56,9 +80,9 @@ export async function loadPromptAsset(
5680
}
5781

5882
if (mode !== 'compiled-only') {
59-
const sourceFile = resolve(config.sourceDir, promptPath + '.md');
83+
const sourceFile = resolve(resolvedConfig.sourceDir, promptPath + '.md');
6084

61-
if (config.cache !== false) {
85+
if (resolvedConfig.cache !== false) {
6286
const cached = promptCache.get(sourceFile);
6387
if (cached) {
6488
return cached;
@@ -67,18 +91,18 @@ export async function loadPromptAsset(
6791

6892
if (!existsSync(sourceFile)) {
6993
const paths = [sourceFile];
70-
if (config.compiledDir) {
71-
paths.unshift(resolve(config.compiledDir, promptPath + '.json'));
94+
if (resolvedConfig.compiledDir) {
95+
paths.unshift(resolve(resolvedConfig.compiledDir, promptPath + '.json'));
7296
}
7397

7498
throw new Error(
7599
`Prompt not found: "${promptPath}"\nSearched:\n${paths.map((candidate) => ` - ${candidate}`).join('\n')}`,
76100
);
77101
}
78102

79-
const { asset } = await loadPromptFile(sourceFile, { defaultsRoot: config.sourceDir });
103+
const { asset } = await loadPromptFile(sourceFile, { defaultsRoot: resolvedConfig.sourceDir });
80104

81-
if (config.cache !== false) {
105+
if (resolvedConfig.cache !== false) {
82106
promptCache.set(sourceFile, asset);
83107
}
84108

@@ -94,9 +118,10 @@ export async function resolvePromptAsset(
94118
options: { environment?: string; tier?: string; runtime?: Partial<PromptAssetOverrides> } = {},
95119
promptCache: PromptCache<PromptAsset> = sharedPromptCache,
96120
): Promise<ResolvedPromptAsset> {
97-
let asset = await loadPromptAsset(promptPath, config, promptCache);
121+
const resolvedConfig = withPromptResolutionDefaults(config);
122+
let asset = await loadPromptAsset(promptPath, resolvedConfig, promptCache);
98123

99-
const sourceFile = resolve(config.sourceDir, promptPath + '.md');
124+
const sourceFile = resolve(resolvedConfig.sourceDir, promptPath + '.md');
100125
if (asset.includes && asset.includes.length > 0 && existsSync(sourceFile)) {
101126
asset = await resolveIncludes(asset, sourceFile);
102127
}

src/providers/prompt-input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
type SyncProviderAdapter = Omit<ProviderAdapter, 'validatePrompt' | 'renderPrompt'>;
1212

1313
function isPromptLookup(input: ProviderPromptInput): input is Extract<ProviderPromptInput, { path: string }> {
14-
return 'path' in input && typeof input.path === 'string' && 'sourceDir' in input;
14+
return 'path' in input && typeof input.path === 'string';
1515
}
1616

1717
function isInlinePromptSource(input: ProviderPromptInput): input is Extract<ProviderPromptInput, { source: string }> {

src/providers/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface RuntimeRenderOptions {
3434

3535
export interface ProviderPromptLookup {
3636
path: string;
37-
sourceDir: string;
37+
sourceDir?: string;
3838
compiledDir?: string;
3939
mode?: PromptResolutionMode;
4040
cache?: boolean;

0 commit comments

Comments
 (0)