@anythingai/teleprompt 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # teleprompt
2
2
 
3
- Compose LLM system prompts from discrete sections instead of monolithic template literals.
3
+ Composable, section-based LLM system prompts.
4
4
 
5
- Conditional logic, variants, and prompt changes stay co-located with their content. Adding a new flag is one section in one file, not a boolean threaded through 15 function signatures.
5
+ Each section owns its content and conditional logic. Sections can be reused across prompt variants, conditionally included, and rendered as plain text or XML.
6
6
 
7
7
  ```bash
8
8
  pnpm add @anythingai/teleprompt
@@ -11,35 +11,30 @@ pnpm add @anythingai/teleprompt
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { PromptBuilder, type PromptContext, type PromptSection } from '@anythingai/teleprompt';
14
+ import { PromptBuilder, section, type PromptContext } from '@anythingai/teleprompt';
15
15
 
16
16
  // Define your context shape
17
17
  type MyFlags = { webSearchEnabled: boolean };
18
18
  type MyVars = { assistantName: string };
19
19
  type MyContext = PromptContext<MyFlags, MyVars>;
20
20
 
21
- // Sections are objects with an id and a render function
22
- const identity: PromptSection<MyContext> = {
23
- id: 'identity',
24
- render: (ctx) => `You are ${ctx.vars.assistantName}, a helpful AI assistant.`,
25
- };
26
-
27
- // This is a static section with no context dependencies
28
- const guidelines: PromptSection<MyContext> = {
29
- id: 'guidelines',
30
- render: () => `# Guidelines
21
+ // Static section no context needed, no type parameter
22
+ const guidelines = section('guidelines', () => `# Guidelines
31
23
  - Be concise and direct.
32
24
  - Cite sources when making factual claims.
33
- - Ask for clarification when a request is ambiguous.`,
34
- };
25
+ - Ask for clarification when a request is ambiguous.`);
35
26
 
36
- // Conditional logic lives in the section, not threaded through function signatures
37
- const webSearch: PromptSection<MyContext> = {
38
- id: 'web-search',
39
- when: (ctx) => ctx.flags.webSearchEnabled,
40
- render: () => `You have access to web search. Use it when the user asks about
41
- current events or information that may have changed after your training cutoff.`,
42
- };
27
+ // Dynamic section uses context
28
+ const identity = section('identity', (ctx: MyContext) =>
29
+ `You are ${ctx.vars.assistantName}, a helpful AI assistant.`
30
+ );
31
+
32
+ // Conditional section return null to exclude
33
+ const webSearch = section('web-search', (ctx: MyContext) => {
34
+ if (!ctx.flags.webSearchEnabled) return null;
35
+ return `You have access to web search. Use it when the user asks about
36
+ current events or information that may have changed after your training cutoff.`;
37
+ });
43
38
 
44
39
  // Compose and build
45
40
  const prompt = new PromptBuilder<MyContext>()
@@ -54,21 +49,60 @@ const prompt = new PromptBuilder<MyContext>()
54
49
 
55
50
  ## Sections
56
51
 
57
- A section has an `id`, a `render` function, and optionally a `when` guard:
52
+ `section(id, render)` takes an id and a render function. Return a string to include, `null` to exclude:
58
53
 
59
54
  ```ts
60
- const citation: PromptSection<MyContext> = {
61
- id: 'citation',
62
- when: (ctx) => ctx.flags.citationEnabled, // excluded when false
63
- render: () => 'Always include citations with links when referencing external sources.',
64
- };
55
+ // Always included
56
+ const rules = section('rules', () => 'Be helpful and concise.');
57
+
58
+ // Conditional null means excluded
59
+ const citation = section('citation', (ctx: MyContext) => {
60
+ if (!ctx.flags.citationEnabled) return null;
61
+ return 'Always include citations with links when referencing external sources.';
62
+ });
65
63
  ```
66
64
 
67
65
  Sections render in the order you call `.use()`. To reorder, change the call order.
68
66
 
67
+ Static sections (no type parameter) work in any builder:
68
+
69
+ ```ts
70
+ const disclaimer = section('disclaimer', () => 'Responses are not legal advice.');
71
+
72
+ // Works in any builder regardless of context type
73
+ new PromptBuilder<MyContext>().use(disclaimer)
74
+ new PromptBuilder<OtherContext>().use(disclaimer)
75
+ ```
76
+
77
+ ## Context
78
+
79
+ Sections receive a typed context with boolean flags and arbitrary variables:
80
+
81
+ ```ts
82
+ type MyFlags = {
83
+ webSearchEnabled: boolean;
84
+ citationEnabled: boolean;
85
+ };
86
+
87
+ type MyVars = {
88
+ assistantName: string;
89
+ language: string;
90
+ };
91
+
92
+ type MyContext = PromptContext<MyFlags, MyVars>;
93
+ ```
94
+
95
+ `PromptContext` and `PromptBuilder` have defaults, so the type parameter is optional:
96
+
97
+ ```ts
98
+ const builder = new PromptBuilder();
99
+ ```
100
+
101
+ Build the context once and pass it to `.build(ctx)`. Every section receives the same object.
102
+
69
103
  ## Forking
70
104
 
71
- Create variants without duplicating prompt code:
105
+ Create variants from a shared base:
72
106
 
73
107
  ```ts
74
108
  const base = new PromptBuilder<MyContext>()
@@ -91,50 +125,96 @@ const codeAssistant = base.fork()
91
125
 
92
126
  Each fork is independent. Modifying one doesn't affect the others.
93
127
 
94
- ## Context
128
+ ## XML Format
95
129
 
96
- Sections receive a typed context with boolean flags and arbitrary variables:
130
+ Both Claude and Gemini recommend structuring prompts with XML tags. Pass `{ format: 'xml' }` to `.build()` to wrap each section in `<id>` tags:
97
131
 
98
132
  ```ts
99
- type MyFlags = {
100
- webSearchEnabled: boolean;
101
- citationEnabled: boolean;
102
- };
133
+ builder.build(ctx, { format: 'xml' })
134
+ ```
103
135
 
104
- type MyVars = {
105
- assistantName: string;
106
- language: string;
107
- };
136
+ The section id becomes the tag name. Content is left as-is inside the tags.
108
137
 
109
- type MyContext = PromptContext<MyFlags, MyVars>;
138
+ ## Groups
139
+
140
+ Group related sections together. In text mode, groups are transparent. In XML mode, they wrap children in tags:
141
+
142
+ ```ts
143
+ const prompt = new PromptBuilder<MyContext>()
144
+ .use(identity)
145
+ .group('tools', b => b
146
+ .use(webSearch)
147
+ .use(calculator)
148
+ )
149
+ .use(guidelines)
150
+ .build(ctx, { format: 'xml' });
110
151
  ```
111
152
 
112
- You build the context once and pass it to `.build(ctx)`. Every section receives the same object — no threading booleans through function signatures.
153
+ ```xml
154
+ <identity>
155
+ You are Daniel, a helpful AI assistant.
156
+ </identity>
157
+
158
+ <tools>
159
+ <web-search>
160
+ You have access to web search...
161
+ </web-search>
162
+
163
+ <calculator>
164
+ You can evaluate math expressions...
165
+ </calculator>
166
+ </tools>
167
+
168
+ <guidelines>
169
+ # Guidelines
170
+ ...
171
+ </guidelines>
172
+ ```
113
173
 
114
- ## Builder API
174
+ Groups can be nested:
115
175
 
116
176
  ```ts
117
- new PromptBuilder<MyContext>()
118
- // append a section (replaces if same id exists)
119
- .use(section)
177
+ builder.group('capabilities', b => b
178
+ .group('tools', b => b
179
+ .use(webSearch)
180
+ .use(calculator)
181
+ )
182
+ .group('integrations', b => b
183
+ .use(slack)
184
+ .use(linear)
185
+ )
186
+ )
187
+ ```
120
188
 
121
- // remove by section object or string id
122
- .without(section)
189
+ ## Mutually Exclusive Sections
123
190
 
124
- // check existence by section object or string id
125
- .has(section)
191
+ Use `.useOneOf()` when exactly one of several sections should render. The first candidate that returns a non-empty string wins:
126
192
 
127
- // list all section ids
128
- .ids()
193
+ ```ts
194
+ const hasTasks = section('has-tasks', (ctx: MyContext) => {
195
+ if (ctx.vars.tasks.length === 0) return null;
196
+ return `## Active Tasks\n\n${ctx.vars.tasks.map(t => `- ${t.title}`).join('\n')}`;
197
+ });
129
198
 
130
- // independent copy
131
- .fork()
199
+ const noTasks = section('no-tasks', () => '## Active Tasks\n\nNo tasks currently running.');
132
200
 
133
- // render to string
134
- .build(ctx)
201
+ builder.useOneOf(hasTasks, noTasks);
202
+ ```
135
203
 
136
- // render + debug info: { included: string[], excluded: string[] }
137
- .buildWithMeta(ctx)
204
+ ## Builder API
205
+
206
+ ```ts
207
+ new PromptBuilder<MyContext>()
208
+ .use(section) // append (replaces if same id)
209
+ .useOneOf(sectionA, sectionB) // first match wins
210
+ .group('name', b => b.use(...)) // named group (XML wrapper)
211
+ .without(section) // remove by object or string id
212
+ .has(section) // check existence
213
+ .ids() // list all section ids
214
+ .fork() // independent copy
215
+ .build(ctx) // render to string
216
+ .build(ctx, { format: 'xml' }) // render with XML tags
217
+ .buildWithMeta(ctx) // render + { included, excluded }
138
218
  ```
139
219
 
140
220
  ## Testing
package/dist/index.cjs CHANGED
@@ -1,95 +1,238 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); var _class;// src/builder.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/builder.ts
2
2
  var PromptBuilder = (_class = class _PromptBuilder {constructor() { _class.prototype.__init.call(this); }
3
- __init() {this.sections = []}
3
+ __init() {this.nodes = []}
4
+ /** Replaces an existing section with the same id. */
5
+ use(section2) {
6
+ const idx = this.nodes.findIndex((n) => !isOneOf(n) && n.id === section2.id);
7
+ if (idx >= 0) {
8
+ this.nodes[idx] = section2;
9
+ } else {
10
+ this.nodes.push(section2);
11
+ }
12
+ return this;
13
+ }
4
14
  /**
5
- * Add a section to the prompt.
15
+ * First candidate that renders a non-empty string wins.
6
16
  *
7
- * If a section with the same `id` already exists, it is replaced.
8
- * This makes `.use()` idempotent — you can safely call it multiple
9
- * times with the same section without creating duplicates.
17
+ * @example
18
+ * ```ts
19
+ * builder.useOneOf(activeTasks, noActiveTasks)
20
+ * ```
10
21
  */
11
- use(section) {
12
- const existingIdx = this.sections.findIndex((s) => s.id === section.id);
13
- if (existingIdx >= 0) {
14
- this.sections[existingIdx] = section;
22
+ useOneOf(...candidates) {
23
+ this.nodes.push({ candidates });
24
+ return this;
25
+ }
26
+ /**
27
+ * In `xml` format, wraps children in `<id>` tags. Transparent in `text` format.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * builder.group('tools', b => b
32
+ * .use(bashSection)
33
+ * .use(gitSection)
34
+ * )
35
+ * ```
36
+ */
37
+ group(id, configure) {
38
+ const inner = new _PromptBuilder();
39
+ configure(inner);
40
+ const group = { id, children: inner.nodes };
41
+ const idx = this.nodes.findIndex((n) => !isOneOf(n) && n.id === id);
42
+ if (idx >= 0) {
43
+ this.nodes[idx] = group;
15
44
  } else {
16
- this.sections.push(section);
45
+ this.nodes.push(group);
17
46
  }
18
47
  return this;
19
48
  }
20
- /** Remove a section. Accepts an id string or a section object. */
49
+ /** Searches recursively into groups and oneOf candidates. */
21
50
  without(ref) {
22
51
  const id = typeof ref === "string" ? ref : ref.id;
23
- this.sections = this.sections.filter((s) => s.id !== id);
52
+ this.nodes = removeNode(this.nodes, id);
24
53
  return this;
25
54
  }
26
- /** Check if a section exists. Accepts an id string or a section object. */
55
+ /** Searches recursively into groups and oneOf candidates. */
27
56
  has(ref) {
28
57
  const id = typeof ref === "string" ? ref : ref.id;
29
- return this.sections.some((s) => s.id === id);
58
+ return hasNode(this.nodes, id);
30
59
  }
31
- /** Get the ids of all registered sections (in insertion order). */
32
60
  ids() {
33
- return this.sections.map((s) => s.id);
61
+ return collectIds(this.nodes);
34
62
  }
35
63
  /**
36
- * Create an independent copy of this builder.
37
- *
38
- * Use this to create mode-specific or model-specific variants
39
- * without mutating the base builder.
64
+ * Creates an independent copy. Modifications to the fork don't affect the original.
40
65
  *
41
66
  * @example
42
67
  * ```ts
43
68
  * const base = new PromptBuilder().use(a).use(b);
44
- * const variant = base.fork().use(c); // base is unchanged
69
+ * const variant = base.fork().use(c);
45
70
  * ```
46
71
  */
47
72
  fork() {
48
73
  const forked = new _PromptBuilder();
49
- forked.sections = [...this.sections];
74
+ forked.nodes = deepCopy(this.nodes);
50
75
  return forked;
51
76
  }
52
- /**
53
- * Build the final prompt string.
54
- *
55
- * 1. Filters out sections whose `when` guard returns false
56
- * 2. Renders each section
57
- * 3. Filters out empty strings
58
- * 4. Joins with separator and trims
59
- */
60
- build(ctx) {
61
- return this.buildWithMeta(ctx).prompt;
77
+ build(ctx, options) {
78
+ return this.buildWithMeta(ctx, options).prompt;
62
79
  }
63
- /**
64
- * Build the prompt and return metadata about which sections were
65
- * included/excluded. Useful for debugging and logging.
66
- *
67
- * A section is "excluded" if its `when` guard returns false.
68
- * A section is "included" only if it passes the guard and renders
69
- * a non-empty string.
70
- */
71
- buildWithMeta(ctx) {
80
+ /** Like `build`, but also returns which section ids were included/excluded. */
81
+ buildWithMeta(ctx, options) {
72
82
  const included = [];
73
83
  const excluded = [];
74
- const rendered = this.sections.filter((s) => {
75
- if (s.when && !s.when(ctx)) {
76
- excluded.push(s.id);
77
- return false;
84
+ const format = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _ => _.format]), () => ( "text"));
85
+ const parts = renderNodes(this.nodes, ctx, format, included, excluded);
86
+ const prompt = parts.join("\n\n").trim();
87
+ return { prompt, included, excluded };
88
+ }
89
+ }, _class);
90
+ function isSection(node) {
91
+ return "render" in node;
92
+ }
93
+ function isGroup(node) {
94
+ return "children" in node;
95
+ }
96
+ function isOneOf(node) {
97
+ return "candidates" in node;
98
+ }
99
+ function assertNever(value) {
100
+ throw new Error(`Unexpected format: ${value}`);
101
+ }
102
+ function formatSection(id, content, format) {
103
+ switch (format) {
104
+ case "text":
105
+ return content;
106
+ case "xml":
107
+ return `<${id}>
108
+ ${content}
109
+ </${id}>`;
110
+ default:
111
+ return assertNever(format);
112
+ }
113
+ }
114
+ function formatGroup(id, childParts, format) {
115
+ switch (format) {
116
+ case "text":
117
+ return childParts;
118
+ case "xml":
119
+ return [`<${id}>
120
+ ${childParts.join("\n\n")}
121
+ </${id}>`];
122
+ default:
123
+ return assertNever(format);
124
+ }
125
+ }
126
+ function renderSection(section2, ctx, format, parts, included, excluded) {
127
+ if (section2.when && !section2.when(ctx)) {
128
+ excluded.push(section2.id);
129
+ return false;
130
+ }
131
+ const output = section2.render(ctx);
132
+ if (output) {
133
+ included.push(section2.id);
134
+ parts.push(formatSection(section2.id, output, format));
135
+ return true;
136
+ }
137
+ excluded.push(section2.id);
138
+ return false;
139
+ }
140
+ function renderOneOf(node, ctx, format, parts, included, excluded) {
141
+ let found = false;
142
+ for (const candidate of node.candidates) {
143
+ if (found) {
144
+ excluded.push(candidate.id);
145
+ continue;
146
+ }
147
+ if (renderSection(candidate, ctx, format, parts, included, excluded)) {
148
+ found = true;
149
+ }
150
+ }
151
+ }
152
+ function renderNodes(nodes, ctx, format, included, excluded) {
153
+ const parts = [];
154
+ for (const node of nodes) {
155
+ if (isSection(node)) {
156
+ renderSection(node, ctx, format, parts, included, excluded);
157
+ } else if (isGroup(node)) {
158
+ const childParts = renderNodes(node.children, ctx, format, included, excluded);
159
+ if (childParts.length > 0) {
160
+ parts.push(...formatGroup(node.id, childParts, format));
78
161
  }
79
- return true;
80
- }).map((s) => {
81
- const output = s.render(ctx);
82
- if (output) {
83
- included.push(s.id);
84
- } else {
85
- excluded.push(s.id);
162
+ } else {
163
+ renderOneOf(node, ctx, format, parts, included, excluded);
164
+ }
165
+ }
166
+ return parts;
167
+ }
168
+ function hasNode(nodes, id) {
169
+ for (const node of nodes) {
170
+ if (isSection(node)) {
171
+ if (node.id === id) return true;
172
+ } else if (isGroup(node)) {
173
+ if (node.id === id) return true;
174
+ if (hasNode(node.children, id)) return true;
175
+ } else {
176
+ for (const c of node.candidates) {
177
+ if (c.id === id) return true;
86
178
  }
87
- return output;
88
- }).filter(Boolean).join("\n\n").trim();
89
- return { prompt: rendered, included, excluded };
179
+ }
90
180
  }
91
- }, _class);
181
+ return false;
182
+ }
183
+ function removeNode(nodes, id) {
184
+ const result = [];
185
+ for (const n of nodes) {
186
+ if (isSection(n)) {
187
+ if (n.id !== id) result.push(n);
188
+ } else if (isGroup(n)) {
189
+ if (n.id !== id) {
190
+ result.push({ ...n, children: removeNode(n.children, id) });
191
+ }
192
+ } else {
193
+ const remaining = n.candidates.filter((c) => c.id !== id);
194
+ if (remaining.length > 0) {
195
+ result.push({ candidates: remaining });
196
+ }
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+ function collectIds(nodes) {
202
+ const ids = [];
203
+ for (const node of nodes) {
204
+ if (isSection(node)) {
205
+ ids.push(node.id);
206
+ } else if (isGroup(node)) {
207
+ ids.push(node.id);
208
+ ids.push(...collectIds(node.children));
209
+ } else {
210
+ ids.push(...node.candidates.map((c) => c.id));
211
+ }
212
+ }
213
+ return ids;
214
+ }
215
+ function deepCopy(nodes) {
216
+ return nodes.map((n) => {
217
+ if (isGroup(n)) {
218
+ return { ...n, children: deepCopy(n.children) };
219
+ }
220
+ if (isOneOf(n)) {
221
+ return { candidates: [...n.candidates] };
222
+ }
223
+ return n;
224
+ });
225
+ }
226
+
227
+ // src/section.ts
228
+ function section(id, render) {
229
+ return {
230
+ id,
231
+ render: (ctx) => _nullishCoalesce(render(ctx), () => ( ""))
232
+ };
233
+ }
234
+
92
235
 
93
236
 
94
- exports.PromptBuilder = PromptBuilder;
237
+ exports.PromptBuilder = PromptBuilder; exports.section = section;
95
238
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/asurve/dev/teleprompt/dist/index.cjs","../src/builder.ts"],"names":[],"mappings":"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","file":"/Users/asurve/dev/teleprompt/dist/index.cjs","sourcesContent":[null,"import type { PromptContext, PromptSection } from './types';\n\n/**\n * Declarative, composable prompt builder.\n *\n * Prompts are composed from discrete {@link PromptSection}s that are\n * independently testable pure functions. The builder handles ordering,\n * conditional inclusion, and final assembly.\n *\n * @example\n * ```ts\n * const prompt = new PromptBuilder()\n * .use(identitySection)\n * .use(rulesSection)\n * .use(featureSection)\n * .build(ctx);\n * ```\n */\nexport class PromptBuilder<\n TCtx extends PromptContext<Record<string, boolean>, Record<string, unknown>>,\n> {\n private sections: PromptSection<TCtx>[] = [];\n\n /**\n * Add a section to the prompt.\n *\n * If a section with the same `id` already exists, it is replaced.\n * This makes `.use()` idempotent — you can safely call it multiple\n * times with the same section without creating duplicates.\n */\n use(section: PromptSection<TCtx>): this {\n const existingIdx = this.sections.findIndex((s) => s.id === section.id);\n if (existingIdx >= 0) {\n this.sections[existingIdx] = section;\n } else {\n this.sections.push(section);\n }\n return this;\n }\n\n /** Remove a section. Accepts an id string or a section object. */\n without(ref: string | { id: string }): this {\n const id = typeof ref === 'string' ? ref : ref.id;\n this.sections = this.sections.filter((s) => s.id !== id);\n return this;\n }\n\n /** Check if a section exists. Accepts an id string or a section object. */\n has(ref: string | { id: string }): boolean {\n const id = typeof ref === 'string' ? ref : ref.id;\n return this.sections.some((s) => s.id === id);\n }\n\n /** Get the ids of all registered sections (in insertion order). */\n ids(): string[] {\n return this.sections.map((s) => s.id);\n }\n\n /**\n * Create an independent copy of this builder.\n *\n * Use this to create mode-specific or model-specific variants\n * without mutating the base builder.\n *\n * @example\n * ```ts\n * const base = new PromptBuilder().use(a).use(b);\n * const variant = base.fork().use(c); // base is unchanged\n * ```\n */\n fork(): PromptBuilder<TCtx> {\n const forked = new PromptBuilder<TCtx>();\n forked.sections = [...this.sections];\n return forked;\n }\n\n /**\n * Build the final prompt string.\n *\n * 1. Filters out sections whose `when` guard returns false\n * 2. Renders each section\n * 3. Filters out empty strings\n * 4. Joins with separator and trims\n */\n build(ctx: TCtx): string {\n return this.buildWithMeta(ctx).prompt;\n }\n\n /**\n * Build the prompt and return metadata about which sections were\n * included/excluded. Useful for debugging and logging.\n *\n * A section is \"excluded\" if its `when` guard returns false.\n * A section is \"included\" only if it passes the guard and renders\n * a non-empty string.\n */\n buildWithMeta(ctx: TCtx): {\n prompt: string;\n included: string[];\n excluded: string[];\n } {\n const included: string[] = [];\n const excluded: string[] = [];\n\n const rendered = this.sections\n .filter((s) => {\n if (s.when && !s.when(ctx)) {\n excluded.push(s.id);\n return false;\n }\n return true;\n })\n .map((s) => {\n const output = s.render(ctx);\n if (output) {\n included.push(s.id);\n } else {\n excluded.push(s.id);\n }\n return output;\n })\n .filter(Boolean)\n .join('\\n\\n')\n .trim();\n\n return { prompt: rendered, included, excluded };\n }\n}\n"]}
1
+ {"version":3,"sources":["/Users/asurve/dev/teleprompt/main/dist/index.cjs","../src/builder.ts","../src/section.ts"],"names":["section"],"mappings":"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","file":"/Users/asurve/dev/teleprompt/main/dist/index.cjs","sourcesContent":[null,"import type { PromptContext, PromptSection } from './types';\n\nexport type PromptFormat = 'text' | 'xml';\n\nexport interface BuildOptions {\n /**\n * - `'text'` (default) — sections joined with `\\n\\n`, groups are transparent.\n * - `'xml'` — each section wrapped in `<id>...</id>` tags, groups\n * wrapped in `<id>...</id>` containing their children.\n */\n format?: PromptFormat;\n}\n\n/**\n * @example\n * ```ts\n * const prompt = new PromptBuilder()\n * .use(identitySection)\n * .use(rulesSection)\n * .use(featureSection)\n * .build(ctx);\n * ```\n */\nexport class PromptBuilder<TCtx extends PromptContext = PromptContext> {\n private nodes: PromptNode<TCtx>[] = [];\n\n /** Replaces an existing section with the same id. */\n use(section: PromptSection<TCtx>): this {\n const idx = this.nodes.findIndex((n) => !isOneOf(n) && n.id === section.id);\n if (idx >= 0) {\n this.nodes[idx] = section;\n } else {\n this.nodes.push(section);\n }\n return this;\n }\n\n /**\n * First candidate that renders a non-empty string wins.\n *\n * @example\n * ```ts\n * builder.useOneOf(activeTasks, noActiveTasks)\n * ```\n */\n useOneOf(...candidates: PromptSection<TCtx>[]): this {\n this.nodes.push({ candidates });\n return this;\n }\n\n /**\n * In `xml` format, wraps children in `<id>` tags. Transparent in `text` format.\n *\n * @example\n * ```ts\n * builder.group('tools', b => b\n * .use(bashSection)\n * .use(gitSection)\n * )\n * ```\n */\n group(id: string, configure: (builder: PromptBuilder<TCtx>) => void): this {\n const inner = new PromptBuilder<TCtx>();\n configure(inner);\n const group: PromptGroup<TCtx> = { id, children: inner.nodes };\n const idx = this.nodes.findIndex((n) => !isOneOf(n) && n.id === id);\n if (idx >= 0) {\n this.nodes[idx] = group;\n } else {\n this.nodes.push(group);\n }\n return this;\n }\n\n /** Searches recursively into groups and oneOf candidates. */\n without(ref: string | { id: string }): this {\n const id = typeof ref === 'string' ? ref : ref.id;\n this.nodes = removeNode(this.nodes, id);\n return this;\n }\n\n /** Searches recursively into groups and oneOf candidates. */\n has(ref: string | { id: string }): boolean {\n const id = typeof ref === 'string' ? ref : ref.id;\n return hasNode(this.nodes, id);\n }\n\n ids(): string[] {\n return collectIds(this.nodes);\n }\n\n /**\n * Creates an independent copy. Modifications to the fork don't affect the original.\n *\n * @example\n * ```ts\n * const base = new PromptBuilder().use(a).use(b);\n * const variant = base.fork().use(c);\n * ```\n */\n fork(): PromptBuilder<TCtx> {\n const forked = new PromptBuilder<TCtx>();\n forked.nodes = deepCopy(this.nodes);\n return forked;\n }\n\n build(ctx: TCtx, options?: BuildOptions): string {\n return this.buildWithMeta(ctx, options).prompt;\n }\n\n /** Like `build`, but also returns which section ids were included/excluded. */\n buildWithMeta(\n ctx: TCtx,\n options?: BuildOptions,\n ): {\n prompt: string;\n included: string[];\n excluded: string[];\n } {\n const included: string[] = [];\n const excluded: string[] = [];\n const format = options?.format ?? 'text';\n\n const parts = renderNodes(this.nodes, ctx, format, included, excluded);\n const prompt = parts.join('\\n\\n').trim();\n\n return { prompt, included, excluded };\n }\n}\n\ninterface PromptGroup<TCtx extends PromptContext> {\n id: string;\n children: PromptNode<TCtx>[];\n}\n\ninterface PromptOneOf<TCtx extends PromptContext> {\n candidates: PromptSection<TCtx>[];\n}\n\ntype PromptNode<TCtx extends PromptContext> =\n | PromptSection<TCtx>\n | PromptGroup<TCtx>\n | PromptOneOf<TCtx>;\n\nfunction isSection<TCtx extends PromptContext>(\n node: PromptNode<TCtx>,\n): node is PromptSection<TCtx> {\n return 'render' in node;\n}\n\nfunction isGroup<TCtx extends PromptContext>(node: PromptNode<TCtx>): node is PromptGroup<TCtx> {\n return 'children' in node;\n}\n\nfunction isOneOf<TCtx extends PromptContext>(node: PromptNode<TCtx>): node is PromptOneOf<TCtx> {\n return 'candidates' in node;\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unexpected format: ${value}`);\n}\n\nfunction formatSection(id: string, content: string, format: PromptFormat): string {\n switch (format) {\n case 'text':\n return content;\n case 'xml':\n return `<${id}>\\n${content}\\n</${id}>`;\n default:\n return assertNever(format);\n }\n}\n\nfunction formatGroup(id: string, childParts: string[], format: PromptFormat): string[] {\n switch (format) {\n case 'text':\n return childParts;\n case 'xml':\n return [`<${id}>\\n${childParts.join('\\n\\n')}\\n</${id}>`];\n default:\n return assertNever(format);\n }\n}\n\nfunction renderSection<TCtx extends PromptContext>(\n section: PromptSection<TCtx>,\n ctx: TCtx,\n format: PromptFormat,\n parts: string[],\n included: string[],\n excluded: string[],\n): boolean {\n if (section.when && !section.when(ctx)) {\n excluded.push(section.id);\n return false;\n }\n const output = section.render(ctx);\n if (output) {\n included.push(section.id);\n parts.push(formatSection(section.id, output, format));\n return true;\n }\n excluded.push(section.id);\n return false;\n}\n\nfunction renderOneOf<TCtx extends PromptContext>(\n node: PromptOneOf<TCtx>,\n ctx: TCtx,\n format: PromptFormat,\n parts: string[],\n included: string[],\n excluded: string[],\n): void {\n let found = false;\n for (const candidate of node.candidates) {\n if (found) {\n excluded.push(candidate.id);\n continue;\n }\n if (renderSection(candidate, ctx, format, parts, included, excluded)) {\n found = true;\n }\n }\n}\n\nfunction renderNodes<TCtx extends PromptContext>(\n nodes: PromptNode<TCtx>[],\n ctx: TCtx,\n format: PromptFormat,\n included: string[],\n excluded: string[],\n): string[] {\n const parts: string[] = [];\n\n for (const node of nodes) {\n if (isSection(node)) {\n renderSection(node, ctx, format, parts, included, excluded);\n } else if (isGroup(node)) {\n const childParts = renderNodes(node.children, ctx, format, included, excluded);\n if (childParts.length > 0) {\n parts.push(...formatGroup(node.id, childParts, format));\n }\n } else {\n renderOneOf(node, ctx, format, parts, included, excluded);\n }\n }\n\n return parts;\n}\n\nfunction hasNode<TCtx extends PromptContext>(nodes: PromptNode<TCtx>[], id: string): boolean {\n for (const node of nodes) {\n if (isSection(node)) {\n if (node.id === id) return true;\n } else if (isGroup(node)) {\n if (node.id === id) return true;\n if (hasNode(node.children, id)) return true;\n } else {\n for (const c of node.candidates) {\n if (c.id === id) return true;\n }\n }\n }\n return false;\n}\n\nfunction removeNode<TCtx extends PromptContext>(\n nodes: PromptNode<TCtx>[],\n id: string,\n): PromptNode<TCtx>[] {\n const result: PromptNode<TCtx>[] = [];\n for (const n of nodes) {\n if (isSection(n)) {\n if (n.id !== id) result.push(n);\n } else if (isGroup(n)) {\n if (n.id !== id) {\n result.push({ ...n, children: removeNode(n.children, id) });\n }\n } else {\n const remaining = n.candidates.filter((c) => c.id !== id);\n if (remaining.length > 0) {\n result.push({ candidates: remaining });\n }\n }\n }\n return result;\n}\n\nfunction collectIds<TCtx extends PromptContext>(nodes: PromptNode<TCtx>[]): string[] {\n const ids: string[] = [];\n for (const node of nodes) {\n if (isSection(node)) {\n ids.push(node.id);\n } else if (isGroup(node)) {\n ids.push(node.id);\n ids.push(...collectIds(node.children));\n } else {\n ids.push(...node.candidates.map((c) => c.id));\n }\n }\n return ids;\n}\n\nfunction deepCopy<TCtx extends PromptContext>(nodes: PromptNode<TCtx>[]): PromptNode<TCtx>[] {\n return nodes.map((n) => {\n if (isGroup(n)) {\n return { ...n, children: deepCopy(n.children) };\n }\n if (isOneOf(n)) {\n return { candidates: [...n.candidates] };\n }\n return n;\n });\n}\n","import type { PromptContext, PromptSection } from './types';\n\n/**\n * Create a prompt section. Return a string to include, `null` to exclude.\n *\n * @example\n * ```ts\n * section('identity', () => 'You are Coworker.')\n *\n * section('prod', (ctx: SandboxCtx) => {\n * if (ctx.vars.prodContext == null) return null;\n * return `## Prod\\n\\n${ctx.vars.prodContext}`;\n * })\n * ```\n */\nexport function section<TCtx extends PromptContext = PromptContext>(\n id: string,\n render: (ctx: TCtx) => string | null,\n): PromptSection<TCtx> {\n return {\n id,\n render: (ctx) => render(ctx) ?? '',\n };\n}\n"]}