promptloom 1.0.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.
@@ -0,0 +1,388 @@
1
+ # promptloom
2
+
3
+ 为 LLM 应用编织生产级提示词 —— 多区域缓存、条件段、工具注入、延迟加载、Token 预算,一步到位。
4
+
5
+ 从 [Claude Code](https://claude.ai/code) 的 7 层提示词架构逆向工程而来 —— 这正是 Anthropic 内部用来组装其 51 万行 CLI 工具系统提示词的模式。
6
+
7
+ ## 为什么需要它
8
+
9
+ 每个 LLM 应用都在拼接提示词。大多数用字符串拼接。Claude Code 用的是**编译器** —— 多区域缓存范围、条件段、逐工具提示词注入、延迟工具加载、Token 预算追踪。
10
+
11
+ **promptloom** 把这些经过生产验证的模式提炼成零依赖库。
12
+
13
+ | 痛点 | promptloom 的解法 |
14
+ |------|-------------------|
15
+ | 改一段提示词就破坏整个缓存 → 白花钱 | **多区域缓存** —— 每个 zone 有独立的缓存范围(`global`、`org`、`null`)|
16
+ | 工具描述散落各处,难以管理 | **工具注册表**,会话级缓存 + 稳定排序 |
17
+ | 工具太多撑爆系统提示词 | **延迟工具** —— 标记为 deferred 的工具不进提示词,按需加载 |
18
+ | 某些段只和特定模型/环境相关 | **条件段** —— `when` 谓词按编译上下文决定是否包含 |
19
+ | 不知道提示词花了多少 Token | 每次 `compile()` 自动输出 **Token 估算** |
20
+ | 不同 API 提供商格式不同 | **多 Provider 输出** —— `toAnthropic()`、`toOpenAI()`、`toBedrock()` |
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ bun add promptloom
26
+ ```
27
+
28
+ ## 快速上手
29
+
30
+ ```ts
31
+ import { PromptCompiler, toAnthropic } from 'promptloom'
32
+
33
+ const pc = new PromptCompiler()
34
+
35
+ // ── Zone 1: 归属头(不缓存)──
36
+ pc.zone(null)
37
+ pc.static('attribution', 'x-billing-org: org-123')
38
+
39
+ // ── Zone 2: 静态规则(全局可缓存)──
40
+ pc.zone('global')
41
+ pc.static('identity', '你是一个代码审查机器人。')
42
+ pc.static('rules', '只评论 Bug,不评论代码风格。')
43
+
44
+ // ── Zone 3: 动态上下文(会话级,不缓存)──
45
+ pc.zone(null)
46
+ pc.dynamic('diff', async () => {
47
+ const diff = await getCurrentDiff()
48
+ return `审查这段 diff:\n${diff}`
49
+ })
50
+
51
+ // 条件段 —— 仅在 Opus 模型时包含
52
+ pc.static('thinking', '对复杂审查使用扩展思考。', {
53
+ when: (ctx) => ctx.model?.includes('opus') ?? false,
54
+ })
55
+
56
+ // ── 工具(内联 + 延迟)──
57
+ pc.tool({
58
+ name: 'post_comment',
59
+ prompt: '在代码的指定行发布审查评论。',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ file: { type: 'string' },
64
+ line: { type: 'number' },
65
+ body: { type: 'string' },
66
+ },
67
+ required: ['file', 'line', 'body'],
68
+ },
69
+ order: 1, // 显式排序,保证缓存稳定性
70
+ })
71
+
72
+ pc.tool({
73
+ name: 'web_search',
74
+ prompt: '搜索网页获取上下文。',
75
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
76
+ deferred: true, // 不进系统提示词,按需加载
77
+ })
78
+
79
+ // ── 编译(传入上下文用于条件段求值)──
80
+ const result = await pc.compile({ model: 'claude-opus-4-6' })
81
+
82
+ result.blocks // CacheBlock[] — 每个 zone 一个块,带缓存范围标注
83
+ result.tools // CompiledTool[] — 仅内联工具
84
+ result.deferredTools // CompiledTool[] — 延迟工具(带 defer_loading: true)
85
+ result.tokens // { systemPrompt, tools, deferredTools, total }
86
+ result.text // 完整提示词文本
87
+ ```
88
+
89
+ ## 配合各 API 使用
90
+
91
+ ### Anthropic
92
+
93
+ ```ts
94
+ import Anthropic from '@anthropic-ai/sdk'
95
+ import { PromptCompiler, toAnthropic } from 'promptloom'
96
+
97
+ const pc = new PromptCompiler()
98
+ // ... 添加 zone、section、tool ...
99
+
100
+ const result = await pc.compile({ model: 'claude-sonnet-4-6' })
101
+ const { system, tools } = toAnthropic(result) // 带缓存标注的 blocks + 工具 schema
102
+
103
+ const response = await new Anthropic().messages.create({
104
+ model: 'claude-sonnet-4-6',
105
+ max_tokens: 4096,
106
+ system, // TextBlockParam[],带 cache_control
107
+ tools, // 包含延迟工具(带 defer_loading: true)
108
+ messages: [{ role: 'user', content: '审查这个 PR' }],
109
+ })
110
+ ```
111
+
112
+ ### OpenAI
113
+
114
+ ```ts
115
+ import OpenAI from 'openai'
116
+ import { PromptCompiler, toOpenAI } from 'promptloom'
117
+
118
+ const pc = new PromptCompiler()
119
+ // ... 添加 zone、section、tool ...
120
+
121
+ const result = await pc.compile()
122
+ const { system, tools } = toOpenAI(result) // 单字符串 + function 格式工具
123
+
124
+ const response = await new OpenAI().chat.completions.create({
125
+ model: 'gpt-4o',
126
+ messages: [
127
+ { role: 'system', content: system },
128
+ { role: 'user', content: '审查这个 PR' },
129
+ ],
130
+ tools,
131
+ })
132
+ ```
133
+
134
+ ### AWS Bedrock
135
+
136
+ ```ts
137
+ import { PromptCompiler, toBedrock } from 'promptloom'
138
+
139
+ const result = await pc.compile()
140
+ const { system, toolConfig } = toBedrock(result) // cachePoint + toolSpec 格式
141
+
142
+ // 用于 @aws-sdk/client-bedrock-runtime ConverseCommand
143
+ ```
144
+
145
+ ## 核心概念
146
+
147
+ ### Zone:多块缓存范围
148
+
149
+ Claude Code 用一个 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 把提示词分成 2 块。promptloom 将其泛化为 **N 个 zone** —— 每个 zone 编译成独立的 `CacheBlock`,拥有自己的缓存范围。
150
+
151
+ ```ts
152
+ pc.zone(null) // Zone 1: 不缓存(归属头)
153
+ pc.static('header', 'x-billing: org-123')
154
+
155
+ pc.zone('global') // Zone 2: 全局可缓存(身份、规则)
156
+ pc.static('identity', '你是 Claude Code。')
157
+ pc.static('rules', '遵守安全协议。')
158
+
159
+ pc.zone('org') // Zone 3: 组织级可缓存
160
+ pc.static('org_rules', '公司专属规范。')
161
+
162
+ pc.zone(null) // Zone 4: 会话级(动态上下文)
163
+ pc.dynamic('git', async () => `分支: ${await getBranch()}`)
164
+ ```
165
+
166
+ 编译结果为 4 个 `CacheBlock`:
167
+
168
+ ```
169
+ ┌─────────────────────────────┐
170
+ │ x-billing: org-123 │ Block 1 scope=null (不缓存)
171
+ ├─────────────────────────────┤
172
+ │ 你是 Claude Code。 │ Block 2 scope=global (跨组织缓存)
173
+ │ 遵守安全协议。 │
174
+ ├─────────────────────────────┤
175
+ │ 公司专属规范。 │ Block 3 scope=org (组织级缓存)
176
+ ├─────────────────────────────┤
177
+ │ 分支: main │ Block 4 scope=null (会话级)
178
+ └─────────────────────────────┘
179
+ ```
180
+
181
+ `boundary()` 方法保留了向后兼容 —— 在 `enableGlobalCache` 为 true 时等同于 `zone(null)`。
182
+
183
+ ### 条件段
184
+
185
+ Claude Code 里通过 `feature('FLAG')`、`process.env.USER_TYPE`、模型能力来门控段。promptloom 用 `when` 谓词:
186
+
187
+ ```ts
188
+ // 仅 Opus 模型
189
+ pc.static('thinking_guide', '对复杂任务使用扩展思考。', {
190
+ when: (ctx) => ctx.model?.includes('opus') ?? false,
191
+ })
192
+
193
+ // 仅连接了 MCP 服务器时
194
+ pc.dynamic('mcp', async () => fetchMCPInstructions(), {
195
+ when: (ctx) => (ctx.mcpServers as string[])?.length > 0,
196
+ })
197
+
198
+ // 仅内部用户
199
+ pc.static('internal_tools', '你可以访问内部 API。', {
200
+ when: (ctx) => ctx.userType === 'internal',
201
+ })
202
+
203
+ // 谓词在编译时求值
204
+ const result = await pc.compile({
205
+ model: 'claude-opus-4-6',
206
+ mcpServers: ['figma', 'slack'],
207
+ userType: 'internal',
208
+ })
209
+ ```
210
+
211
+ ### 工具提示词注入
212
+
213
+ 每个工具带有面向 LLM 的"使用手册",每个会话解析一次后缓存:
214
+
215
+ ```ts
216
+ pc.tool({
217
+ name: 'Bash',
218
+ prompt: async () => {
219
+ const sandbox = await detectSandbox()
220
+ return `执行 Shell 命令。\n${sandbox ? '在沙箱中运行。' : ''}`
221
+ },
222
+ inputSchema: { /* ... */ },
223
+ order: 1, // 显式排序,保证缓存稳定性
224
+ })
225
+ ```
226
+
227
+ ### 延迟工具
228
+
229
+ 当工具很多时(Claude Code 有 42+),大部分每轮并不需要。延迟工具被排除在系统提示词之外,按需发现:
230
+
231
+ ```ts
232
+ pc.tool({
233
+ name: 'web_search',
234
+ prompt: '搜索网页获取信息。',
235
+ inputSchema: { /* ... */ },
236
+ deferred: true, // 不进系统提示词,通过 tool search 按需加载
237
+ })
238
+
239
+ const result = await pc.compile()
240
+ result.tools // 仅内联工具
241
+ result.deferredTools // 延迟工具(带 defer_loading: true)
242
+ result.tokens.total // 不计算延迟工具的 token
243
+ ```
244
+
245
+ ### 工具排序稳定性
246
+
247
+ 重排工具会改变序列化字节,破坏提示词缓存。用 `order` 保证确定性排序:
248
+
249
+ ```ts
250
+ pc.tool({ name: 'bash', prompt: '...', inputSchema: {}, order: 1 })
251
+ pc.tool({ name: 'read', prompt: '...', inputSchema: {}, order: 2 })
252
+ pc.tool({ name: 'edit', prompt: '...', inputSchema: {}, order: 3 })
253
+ // 没有 `order` 的工具排在最后,按插入顺序
254
+ ```
255
+
256
+ ### Token 预算
257
+
258
+ #### 估算
259
+
260
+ 每次 `compile()` 调用都包含 token 估算:
261
+
262
+ ```ts
263
+ const result = await pc.compile()
264
+ result.tokens.systemPrompt // ~350 tokens
265
+ result.tokens.tools // ~200 tokens(仅内联)
266
+ result.tokens.deferredTools // ~100 tokens(不计入 total)
267
+ result.tokens.total // ~550 tokens(systemPrompt + tools)
268
+ ```
269
+
270
+ #### 预算追踪
271
+
272
+ 用于长时间运行的 Agent 循环:
273
+
274
+ ```ts
275
+ import { createBudgetTracker, checkBudget } from 'promptloom'
276
+
277
+ const tracker = createBudgetTracker()
278
+ const decision = checkBudget(tracker, currentTokens, { budget: 100_000 })
279
+
280
+ if (decision.action === 'continue') {
281
+ // 注入 decision.nudgeMessage 让模型继续工作
282
+ } else {
283
+ // decision.reason: 'budget_reached' | 'diminishing_returns'
284
+ }
285
+ ```
286
+
287
+ #### 从自然语言解析预算
288
+
289
+ 像 Claude Code 一样解析用户指定的预算:
290
+
291
+ ```ts
292
+ import { parseTokenBudget } from 'promptloom'
293
+
294
+ parseTokenBudget('+500k') // 500_000
295
+ parseTokenBudget('spend 2M tokens') // 2_000_000
296
+ parseTokenBudget('+1.5b') // 1_500_000_000
297
+ parseTokenBudget('hello world') // null
298
+ ```
299
+
300
+ ## API 参考
301
+
302
+ ### `PromptCompiler`
303
+
304
+ | 方法 | 描述 |
305
+ |------|------|
306
+ | `zone(scope)` | 开始新的缓存区域(`'global'`、`'org'` 或 `null`)|
307
+ | `boundary()` | `zone(null)` 的简写(需 `enableGlobalCache: true`)|
308
+ | `static(name, content, options?)` | 添加静态段。`options.when` 用于条件包含 |
309
+ | `dynamic(name, compute, options?)` | 添加动态段(每次 `compile()` 重算)|
310
+ | `tool(def)` | 注册工具。`deferred: true` 按需加载,`order` 控制排序 |
311
+ | `compile(context?)` | 编译一切 → `CompileResult`。上下文传给 `when` 谓词 |
312
+ | `clearCache()` | 清除所有段 + 工具缓存 |
313
+ | `clearSectionCache()` | 只清除段缓存 |
314
+ | `clearToolCache()` | 只清除工具缓存 |
315
+ | `sectionCount` | 已注册的段数量(不含 zone 标记)|
316
+ | `toolCount` | 已注册的工具数量(内联 + 延迟)|
317
+ | `listSections()` | 列出所有段及其类型(`static`、`dynamic`、`zone`)|
318
+ | `listTools()` | 列出已注册的工具名 |
319
+
320
+ ### `CompileResult`
321
+
322
+ | 字段 | 类型 | 描述 |
323
+ |------|------|------|
324
+ | `blocks` | `CacheBlock[]` | 每个 zone 一个块,带 `cacheScope` 标注 |
325
+ | `tools` | `CompiledTool[]` | 内联工具 schema(描述已解析)|
326
+ | `deferredTools` | `CompiledTool[]` | 延迟工具 schema(带 `defer_loading: true`)|
327
+ | `tokens` | `TokenEstimate` | `{ systemPrompt, tools, deferredTools, total }` |
328
+ | `text` | `string` | 完整提示词(所有块拼接后的文本)|
329
+
330
+ ### Provider 格式化
331
+
332
+ ```ts
333
+ import { toAnthropic, toOpenAI, toBedrock } from 'promptloom'
334
+
335
+ toAnthropic(result) // { system: TextBlockParam[], tools: AnthropicTool[] }
336
+ toOpenAI(result) // { system: string, tools: { type: 'function', function }[] }
337
+ toBedrock(result) // { system: BedrockSystemBlock[], toolConfig: { tools } }
338
+ ```
339
+
340
+ ### 独立工具函数
341
+
342
+ ```ts
343
+ import {
344
+ // Token 估算
345
+ estimateTokens, // 粗略估算(字节数 / 4)
346
+ estimateTokensForFileType, // 文件类型感知(JSON = 字节数 / 2)
347
+
348
+ // 预算
349
+ createBudgetTracker, // 创建追踪器
350
+ checkBudget, // 检查预算 → 继续或停止
351
+ parseTokenBudget, // 解析 "+500k" → 500_000
352
+
353
+ // 底层工具(用于自定义编译器)
354
+ splitAtBoundary, // 在哨兵处分割文本 → CacheBlock[]
355
+ section, // 创建静态 Section
356
+ dynamicSection, // 创建动态 Section
357
+ defineTool, // 创建 ToolDef(fail-closed 默认值)
358
+ SectionCache, // 段缓存类
359
+ ToolCache, // 工具缓存类
360
+ resolveSections, // 解析段(使用缓存)
361
+ compileTool, // 编译单个工具
362
+ compileTools, // 编译所有工具
363
+ } from 'promptloom'
364
+ ```
365
+
366
+ ## 背景:Claude Code 的提示词架构
367
+
368
+ 本库提取自 Claude Code 的源码(2025 年 3 月通过未剥离的 source map 泄露)。核心洞察:**Anthropic 把提示词当编译器输出来优化,而不是手写文本。**
369
+
370
+ 他们的系统提示词由 7+ 层组装:
371
+
372
+ 1. **身份** — AI 是谁
373
+ 2. **系统** — 工具执行上下文、hooks、压缩机制
374
+ 3. **任务执行** — 代码风格、安全、协作规则
375
+ 4. **行为准则** — 风险感知执行、可逆性考量
376
+ 5. **工具使用** — 工具偏好指引、并行执行
377
+ 6. **语气风格** — 简洁性、格式化规则
378
+ 7. **动态上下文** — Git 状态、CLAUDE.md 文件、用户记忆、MCP 服务器指令
379
+
380
+ 第 1-6 层是**静态的**(全局可缓存)。第 7 层及以后是**动态的**(会话级)。它们之间的边界是一个字面量哨兵字符串,API 层据此标注缓存范围。
381
+
382
+ 段通过特性标志(`feature('TOKEN_BUDGET')`)、用户类型(`process.env.USER_TYPE === 'ant'`)和模型能力条件包含。42+ 个工具中每一个都带有自己的 `prompt.ts`,超过上下文阈值的工具会被延迟(通过 `ToolSearchTool` 按需加载)。
383
+
384
+ promptloom 把这些原语全部交给你。
385
+
386
+ ## 许可
387
+
388
+ MIT
package/bin/cli.ts ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * promptloom CLI — Visualize prompt compilation
4
+ *
5
+ * Usage:
6
+ * promptloom demo Run the built-in demo
7
+ * promptloom inspect <file> Inspect a promptloom config file
8
+ * promptloom tokens <file> Show token estimates
9
+ */
10
+
11
+ import { PromptCompiler, toAnthropicBlocks } from '../src/index.ts'
12
+
13
+ // ─── Colors (ANSI) ──────────────────────────────────────────────
14
+
15
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
16
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
17
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`
18
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`
19
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
20
+ const magenta = (s: string) => `\x1b[35m${s}\x1b[0m`
21
+ const blue = (s: string) => `\x1b[34m${s}\x1b[0m`
22
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`
23
+
24
+ // ─── Demo ────────────────────────────────────────────────────────
25
+
26
+ async function runDemo() {
27
+ console.log(bold('\n promptloom — Prompt Compiler Demo\n'))
28
+ console.log(dim(' Simulating Claude Code\'s 7-layer prompt assembly pattern\n'))
29
+
30
+ const pc = new PromptCompiler()
31
+
32
+ // ── Zone 1: Attribution header (no cache) ──
33
+ pc.zone(null)
34
+ pc.static('attribution', 'x-billing-org: org-demo-123')
35
+
36
+ // ── Zone 2: Static layers (globally cacheable) ──
37
+ pc.zone('global')
38
+ pc.static('identity', [
39
+ '# Identity',
40
+ 'You are Claude Code, an AI coding assistant.',
41
+ 'You help users with software engineering tasks.',
42
+ ].join('\n'))
43
+
44
+ pc.static('system', [
45
+ '# System',
46
+ '- All text you output is displayed to the user.',
47
+ '- Tools are executed in a user-selected permission mode.',
48
+ '- Tool results may include data from external sources.',
49
+ ].join('\n'))
50
+
51
+ pc.static('doing_tasks', [
52
+ '# Doing Tasks',
53
+ '- Read existing code before suggesting modifications.',
54
+ '- Do not create files unless absolutely necessary.',
55
+ '- Be careful not to introduce security vulnerabilities.',
56
+ ].join('\n'))
57
+
58
+ pc.static('actions', [
59
+ '# Executing Actions',
60
+ '- Consider reversibility and blast radius of actions.',
61
+ '- For destructive operations, ask for confirmation.',
62
+ '- Never skip hooks (--no-verify) unless explicitly asked.',
63
+ ].join('\n'))
64
+
65
+ pc.static('tool_usage', [
66
+ '# Using Your Tools',
67
+ '- Use Read instead of cat/head/tail.',
68
+ '- Use Edit instead of sed/awk.',
69
+ '- Use Grep instead of grep/rg.',
70
+ ].join('\n'))
71
+
72
+ pc.static('style', [
73
+ '# Tone & Style',
74
+ '- Keep responses short and concise.',
75
+ '- Only use emojis if explicitly requested.',
76
+ ].join('\n'))
77
+
78
+ // ── Zone 3: Dynamic context (no cache, session-specific) ──
79
+ pc.zone(null)
80
+ pc.dynamic('env', async () => [
81
+ '# Environment',
82
+ `- Working directory: ${process.cwd()}`,
83
+ `- Platform: ${process.platform}`,
84
+ `- Date: ${new Date().toISOString().split('T')[0]}`,
85
+ ].join('\n'))
86
+
87
+ pc.dynamic('git', async () => {
88
+ try {
89
+ const proc = Bun.spawn(['git', 'branch', '--show-current'], {
90
+ stdout: 'pipe',
91
+ stderr: 'pipe',
92
+ })
93
+ const branch = (await new Response(proc.stdout).text()).trim()
94
+ return branch ? `# Git\nCurrent branch: ${branch}` : null
95
+ } catch {
96
+ return null
97
+ }
98
+ })
99
+
100
+ pc.dynamic('memory', async () => [
101
+ '# User Preferences',
102
+ '- Preferred language: TypeScript',
103
+ '- Style: functional, minimal comments',
104
+ ].join('\n'))
105
+
106
+ // Conditional section (only included for Opus models)
107
+ pc.static('thinking_guide', [
108
+ '# Extended Thinking',
109
+ '- Use chain-of-thought for complex reasoning.',
110
+ '- Show your work step by step.',
111
+ ].join('\n'), {
112
+ when: (ctx) => (ctx.model as string)?.includes('opus') ?? false,
113
+ })
114
+
115
+ // ── Tools (inline + deferred) ──
116
+ pc.tool({
117
+ name: 'Bash',
118
+ prompt: [
119
+ 'Execute shell commands in the user\'s environment.',
120
+ '',
121
+ 'Git Safety Protocol:',
122
+ '- NEVER run destructive git commands unless explicitly requested',
123
+ '- NEVER skip hooks (--no-verify)',
124
+ '- Always create NEW commits rather than amending',
125
+ ].join('\n'),
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ command: { type: 'string', description: 'The command to execute' },
130
+ timeout: { type: 'number', description: 'Timeout in milliseconds' },
131
+ },
132
+ required: ['command'],
133
+ },
134
+ })
135
+
136
+ pc.tool({
137
+ name: 'Read',
138
+ prompt: [
139
+ 'Read a file from the local filesystem.',
140
+ '',
141
+ 'Usage:',
142
+ '- The file_path parameter must be an absolute path',
143
+ '- By default, reads up to 2000 lines',
144
+ '- Can read images, PDFs, and Jupyter notebooks',
145
+ ].join('\n'),
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ file_path: { type: 'string', description: 'Absolute path to the file' },
150
+ offset: { type: 'number', description: 'Start line number' },
151
+ limit: { type: 'number', description: 'Number of lines to read' },
152
+ },
153
+ required: ['file_path'],
154
+ },
155
+ })
156
+
157
+ pc.tool({
158
+ name: 'Edit',
159
+ prompt: [
160
+ 'Perform exact string replacements in files.',
161
+ '',
162
+ 'Rules:',
163
+ '- You MUST read the file before editing (enforced)',
164
+ '- The edit will FAIL if old_string is not unique',
165
+ '- Preserve exact indentation',
166
+ ].join('\n'),
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ file_path: { type: 'string' },
171
+ old_string: { type: 'string' },
172
+ new_string: { type: 'string' },
173
+ },
174
+ required: ['file_path', 'old_string', 'new_string'],
175
+ },
176
+ })
177
+
178
+ // ─── Compile ─────────────────────────────────────────────────
179
+
180
+ console.log(dim(' Compiling...\n'))
181
+ const result = await pc.compile()
182
+
183
+ // ─── Display Sections ────────────────────────────────────────
184
+
185
+ console.log(bold(' Sections'))
186
+ console.log(dim(' ─────────────────────────────────────────────'))
187
+ for (const s of pc.listSections()) {
188
+ const icon =
189
+ s.type === 'static' ? green('STATIC ') :
190
+ s.type === 'dynamic' ? yellow('DYNAMIC') :
191
+ magenta('ZONE ')
192
+ const label = s.type === 'zone' ? dim(s.name) : s.name
193
+ console.log(` ${icon} ${label}`)
194
+ }
195
+
196
+ // ─── Display Blocks ──────────────────────────────────────────
197
+
198
+ console.log(bold('\n Cache Blocks'))
199
+ console.log(dim(' ─────────────────────────────────────────────'))
200
+ for (const [i, block] of result.blocks.entries()) {
201
+ const scope =
202
+ block.cacheScope === 'global' ? green('global') :
203
+ block.cacheScope === 'org' ? cyan('org') :
204
+ dim('none')
205
+ const lines = block.text.split('\n').length
206
+ const tokens = Math.round(block.text.length / 4)
207
+ console.log(` Block ${i + 1} scope=${scope} ${dim(`~${tokens} tokens, ${lines} lines`)}`)
208
+
209
+ // Show first 3 lines as preview
210
+ const preview = block.text.split('\n').slice(0, 3)
211
+ for (const line of preview) {
212
+ console.log(` ${dim(line.slice(0, 72))}`)
213
+ }
214
+ if (block.text.split('\n').length > 3) {
215
+ console.log(` ${dim('...')}`)
216
+ }
217
+ }
218
+
219
+ // ─── Display Tools ───────────────────────────────────────────
220
+
221
+ console.log(bold('\n Tools'))
222
+ console.log(dim(' ─────────────────────────────────────────────'))
223
+ for (const tool of result.tools) {
224
+ const promptTokens = Math.round(tool.description.length / 4)
225
+ const schemaTokens = Math.round(JSON.stringify(tool.input_schema).length / 2)
226
+ console.log(` ${blue(tool.name.padEnd(12))} prompt=${dim(`~${promptTokens}t`)} schema=${dim(`~${schemaTokens}t`)}`)
227
+ }
228
+
229
+ // ─── Display Tokens ──────────────────────────────────────────
230
+
231
+ console.log(bold('\n Token Estimates'))
232
+ console.log(dim(' ─────────────────────────────────────────────'))
233
+ console.log(` System prompt: ${cyan(result.tokens.systemPrompt.toLocaleString())} tokens`)
234
+ console.log(` Tool schemas: ${cyan(result.tokens.tools.toLocaleString())} tokens`)
235
+ console.log(` ${bold('Total:')} ${bold(cyan(result.tokens.total.toLocaleString()))} tokens`)
236
+
237
+ // ─── Display Anthropic API Format ────────────────────────────
238
+
239
+ console.log(bold('\n Anthropic API Blocks'))
240
+ console.log(dim(' ─────────────────────────────────────────────'))
241
+ const apiBlocks = toAnthropicBlocks(result.blocks)
242
+ for (const [i, block] of apiBlocks.entries()) {
243
+ const cached = block.cache_control ? green('cached') : dim('no-cache')
244
+ console.log(` [${i}] ${cached} ${dim(`${block.text.length} chars`)}`)
245
+ }
246
+
247
+ console.log(dim('\n Done.\n'))
248
+ }
249
+
250
+ // ─── Main ────────────────────────────────────────────────────────
251
+
252
+ const command = process.argv[2] ?? 'demo'
253
+
254
+ switch (command) {
255
+ case 'demo':
256
+ await runDemo()
257
+ break
258
+ case '--help':
259
+ case '-h':
260
+ console.log(`
261
+ ${bold('promptloom')} — Prompt Compiler
262
+
263
+ ${dim('Usage:')}
264
+ promptloom demo Run the built-in demo
265
+ promptloom --help Show this help
266
+
267
+ ${dim('Library usage:')}
268
+ import { PromptCompiler } from 'promptloom'
269
+ `)
270
+ break
271
+ default:
272
+ console.log(red(`Unknown command: ${command}`))
273
+ console.log(dim('Run `promptloom --help` for usage.'))
274
+ process.exit(1)
275
+ }