@zihanw/pi-forge 0.3.0 → 0.3.2

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 (156) hide show
  1. package/README.md +87 -2
  2. package/README.zh-CN.md +72 -2
  3. package/dist/compiler.d.ts +10 -0
  4. package/dist/compiler.d.ts.map +1 -0
  5. package/dist/compiler.js +490 -0
  6. package/dist/compiler.js.map +1 -0
  7. package/dist/extension-registry.d.ts +25 -0
  8. package/dist/extension-registry.d.ts.map +1 -0
  9. package/dist/extension-registry.js +6 -0
  10. package/dist/extension-registry.js.map +1 -0
  11. package/dist/forge-extensions.d.ts +28 -0
  12. package/dist/forge-extensions.d.ts.map +1 -0
  13. package/dist/forge-extensions.js +182 -0
  14. package/dist/forge-extensions.js.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +376 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lifecycle.d.ts +14 -0
  20. package/dist/lifecycle.d.ts.map +1 -0
  21. package/dist/lifecycle.js +139 -0
  22. package/dist/lifecycle.js.map +1 -0
  23. package/dist/loader.d.ts +8 -0
  24. package/dist/loader.d.ts.map +1 -0
  25. package/dist/loader.js +345 -0
  26. package/dist/loader.js.map +1 -0
  27. package/dist/macro-engine.d.ts +23 -0
  28. package/dist/macro-engine.d.ts.map +1 -0
  29. package/dist/macro-engine.js +262 -0
  30. package/dist/macro-engine.js.map +1 -0
  31. package/dist/payload-capture.d.ts +15 -0
  32. package/dist/payload-capture.d.ts.map +1 -0
  33. package/dist/payload-capture.js +86 -0
  34. package/dist/payload-capture.js.map +1 -0
  35. package/dist/payload-command.d.ts +10 -0
  36. package/dist/payload-command.d.ts.map +1 -0
  37. package/dist/payload-command.js +112 -0
  38. package/dist/payload-command.js.map +1 -0
  39. package/dist/policy.d.ts +6 -0
  40. package/dist/policy.d.ts.map +1 -0
  41. package/dist/policy.js +38 -0
  42. package/dist/policy.js.map +1 -0
  43. package/dist/preset-command.d.ts +14 -0
  44. package/dist/preset-command.d.ts.map +1 -0
  45. package/dist/preset-command.js +232 -0
  46. package/dist/preset-command.js.map +1 -0
  47. package/dist/preview.d.ts +12 -0
  48. package/dist/preview.d.ts.map +1 -0
  49. package/dist/preview.js +179 -0
  50. package/dist/preview.js.map +1 -0
  51. package/dist/regex.d.ts +7 -0
  52. package/dist/regex.d.ts.map +1 -0
  53. package/dist/regex.js +432 -0
  54. package/dist/regex.js.map +1 -0
  55. package/dist/render-helpers.d.ts +44 -0
  56. package/dist/render-helpers.d.ts.map +1 -0
  57. package/dist/render-helpers.js +128 -0
  58. package/dist/render-helpers.js.map +1 -0
  59. package/dist/runtime-state.d.ts +26 -0
  60. package/dist/runtime-state.d.ts.map +1 -0
  61. package/dist/runtime-state.js +15 -0
  62. package/dist/runtime-state.js.map +1 -0
  63. package/dist/sillytavern-importer/items.d.ts +3 -0
  64. package/dist/sillytavern-importer/items.d.ts.map +1 -0
  65. package/dist/sillytavern-importer/items.js +88 -0
  66. package/dist/sillytavern-importer/items.js.map +1 -0
  67. package/dist/sillytavern-importer/macros.d.ts +15 -0
  68. package/dist/sillytavern-importer/macros.d.ts.map +1 -0
  69. package/dist/sillytavern-importer/macros.js +141 -0
  70. package/dist/sillytavern-importer/macros.js.map +1 -0
  71. package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
  72. package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
  73. package/dist/sillytavern-importer/prompt-order.js +38 -0
  74. package/dist/sillytavern-importer/prompt-order.js.map +1 -0
  75. package/dist/sillytavern-importer/regex.d.ts +3 -0
  76. package/dist/sillytavern-importer/regex.d.ts.map +1 -0
  77. package/dist/sillytavern-importer/regex.js +275 -0
  78. package/dist/sillytavern-importer/regex.js.map +1 -0
  79. package/dist/sillytavern-importer/report.d.ts +21 -0
  80. package/dist/sillytavern-importer/report.d.ts.map +1 -0
  81. package/dist/sillytavern-importer/report.js +166 -0
  82. package/dist/sillytavern-importer/report.js.map +1 -0
  83. package/dist/sillytavern-importer/types.d.ts +106 -0
  84. package/dist/sillytavern-importer/types.d.ts.map +1 -0
  85. package/dist/sillytavern-importer/types.js +2 -0
  86. package/dist/sillytavern-importer/types.js.map +1 -0
  87. package/dist/sillytavern-importer.d.ts +5 -0
  88. package/dist/sillytavern-importer.d.ts.map +1 -0
  89. package/dist/sillytavern-importer.js +117 -0
  90. package/dist/sillytavern-importer.js.map +1 -0
  91. package/dist/slot-renderers.d.ts +25 -0
  92. package/dist/slot-renderers.d.ts.map +1 -0
  93. package/dist/slot-renderers.js +334 -0
  94. package/dist/slot-renderers.js.map +1 -0
  95. package/dist/stack-migration.d.ts +29 -0
  96. package/dist/stack-migration.d.ts.map +1 -0
  97. package/dist/stack-migration.js +126 -0
  98. package/dist/stack-migration.js.map +1 -0
  99. package/dist/storage.d.ts +10 -0
  100. package/dist/storage.d.ts.map +1 -0
  101. package/dist/storage.js +36 -0
  102. package/dist/storage.js.map +1 -0
  103. package/dist/types.d.ts +173 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/web-editor/index.d.ts +3 -0
  108. package/dist/web-editor/index.d.ts.map +1 -0
  109. package/dist/web-editor/index.js +2 -0
  110. package/dist/web-editor/index.js.map +1 -0
  111. package/dist/web-editor/page.d.ts +2 -0
  112. package/dist/web-editor/page.d.ts.map +1 -0
  113. package/dist/web-editor/page.js +3331 -0
  114. package/dist/web-editor/page.js.map +1 -0
  115. package/dist/web-editor/server.d.ts +4 -0
  116. package/dist/web-editor/server.d.ts.map +1 -0
  117. package/dist/web-editor/server.js +263 -0
  118. package/dist/web-editor/server.js.map +1 -0
  119. package/dist/web-editor/types.d.ts +125 -0
  120. package/dist/web-editor/types.d.ts.map +1 -0
  121. package/dist/web-editor/types.js +2 -0
  122. package/dist/web-editor/types.js.map +1 -0
  123. package/dist/web-host.d.ts +29 -0
  124. package/dist/web-host.d.ts.map +1 -0
  125. package/dist/web-host.js +174 -0
  126. package/dist/web-host.js.map +1 -0
  127. package/examples/custom-system-status-extension/README.md +85 -0
  128. package/examples/custom-system-status-extension/index.ts +136 -0
  129. package/examples/custom-system-status-extension/prompt-stack.json +39 -0
  130. package/examples/image-reader-prompt-stack.json +124 -0
  131. package/package.json +28 -2
  132. package/src/compiler.ts +2 -472
  133. package/src/extension-registry.ts +33 -0
  134. package/src/forge-extensions.ts +223 -0
  135. package/src/index.ts +208 -653
  136. package/src/lifecycle.ts +171 -0
  137. package/src/macro-engine.ts +358 -0
  138. package/src/payload-command.ts +138 -0
  139. package/src/preset-command.ts +280 -0
  140. package/src/preview.ts +226 -0
  141. package/src/render-helpers.ts +169 -0
  142. package/src/runtime-state.ts +40 -0
  143. package/src/sillytavern-importer/items.ts +98 -0
  144. package/src/sillytavern-importer/macros.ts +159 -0
  145. package/src/sillytavern-importer/prompt-order.ts +54 -0
  146. package/src/sillytavern-importer/regex.ts +270 -0
  147. package/src/sillytavern-importer/report.ts +202 -0
  148. package/src/sillytavern-importer/types.ts +120 -0
  149. package/src/sillytavern-importer.ts +55 -865
  150. package/src/slot-renderers.ts +414 -0
  151. package/src/storage.ts +17 -0
  152. package/src/types.ts +3 -14
  153. package/src/web-editor/page.ts +516 -30
  154. package/src/web-editor/server.ts +9 -4
  155. package/src/web-editor/types.ts +18 -4
  156. package/src/web-host.ts +16 -13
@@ -0,0 +1,414 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { applyResourcePolicy } from "./policy.ts";
4
+ import type {
5
+ PromptRuntime,
6
+ PromptStack,
7
+ PromptStackDiagnostic,
8
+ PromptStackSlotFormat,
9
+ PromptStackSlotItem,
10
+ PromptVariableValue,
11
+ } from "./types.ts";
12
+ import {
13
+ collectStaticVariables,
14
+ createVariableAccess,
15
+ promptRenderHelpers,
16
+ selectedToolNames,
17
+ selectedVariableScopes,
18
+ type PromptRenderHelpers,
19
+ type PromptVariableAccess,
20
+ type PromptVariableScope,
21
+ } from "./render-helpers.ts";
22
+ import { assertRegistryName, type PromptExtensionOptionsSchema, type PromptRegistryEntry } from "./extension-registry.ts";
23
+
24
+ export interface PromptSlotRenderContext {
25
+ item: PromptStackSlotItem;
26
+ stack: PromptStack;
27
+ runtime: PromptRuntime;
28
+ diagnostics: PromptStackDiagnostic[];
29
+ options: Record<string, unknown>;
30
+ helpers: PromptRenderHelpers;
31
+ variables: PromptVariableAccess;
32
+ format: (options?: { allowJson?: boolean }) => PromptStackSlotFormat;
33
+ }
34
+
35
+ export type PromptSlotRenderer = (context: PromptSlotRenderContext) => string | undefined;
36
+
37
+ export interface PromptSlotDefinition extends PromptRegistryEntry {
38
+ options?: PromptExtensionOptionsSchema;
39
+ render: PromptSlotRenderer;
40
+ }
41
+
42
+ interface PromptSlotRegistryState {
43
+ slots: Map<string, PromptSlotDefinition>;
44
+ supportedSlots: Set<string>;
45
+ }
46
+
47
+ type PromptSlotGlobal = typeof globalThis & {
48
+ __piForgeSlotRegistry?: PromptSlotRegistryState;
49
+ };
50
+
51
+ function slotRegistryState(): PromptSlotRegistryState {
52
+ const globalScope = globalThis as PromptSlotGlobal;
53
+ globalScope.__piForgeSlotRegistry ??= { slots: new Map(), supportedSlots: new Set() };
54
+ return globalScope.__piForgeSlotRegistry;
55
+ }
56
+
57
+ const SLOT_REGISTRY = slotRegistryState();
58
+ const SLOT_RENDERERS = SLOT_REGISTRY.slots;
59
+ export const SUPPORTED_SLOTS = SLOT_REGISTRY.supportedSlots;
60
+ let registeringBuiltInSlots = true;
61
+
62
+ const FORMAT_OPTION = {
63
+ type: "enum",
64
+ values: ["xml", "plain"],
65
+ default: "xml",
66
+ description: "Render as XML-style wrappers or compact plain text.",
67
+ } as const;
68
+
69
+ const INCLUDE_TIME_OPTION = {
70
+ type: "boolean",
71
+ default: false,
72
+ description: "Include Current time: HH:MM:SS after the current date.",
73
+ } as const;
74
+
75
+ registerSlot({ name: "chat-history", description: "Conversation history insertion point.", render: () => "" });
76
+ registerSlot({
77
+ name: "tools",
78
+ description: "Available tools and prompt snippets.",
79
+ options: {
80
+ format: FORMAT_OPTION,
81
+ onlyWithSnippets: { type: "boolean", default: false, description: "Only render tools that provide prompt snippets." },
82
+ },
83
+ render: renderTools,
84
+ });
85
+ registerSlot({
86
+ name: "tool-guidelines",
87
+ description: "Tool usage guidelines.",
88
+ options: {
89
+ format: FORMAT_OPTION,
90
+ heading: { type: "string", default: "Tool guidelines:", description: "Plain-format heading." },
91
+ includePiDefaultGuidelines: { type: "boolean", default: false, description: "Include Pi's default concise/file-path guideline bullets." },
92
+ piStyle: { type: "boolean", default: false, description: "Use Pi-style wording for default bash guidance." },
93
+ },
94
+ render: renderToolGuidelines,
95
+ });
96
+ registerSlot({
97
+ name: "skills",
98
+ description: "Available Pi skills.",
99
+ options: {
100
+ format: FORMAT_OPTION,
101
+ requireReadTool: { type: "boolean", default: false, description: "Omit the section unless the read tool is active." },
102
+ },
103
+ render: renderSkills,
104
+ });
105
+ registerSlot({ name: "project-context", description: "Project context files.", options: { format: FORMAT_OPTION }, render: renderProjectContext });
106
+ registerSlot({ name: "append-system-prompt", description: "User appended system prompt text.", render: ({ runtime }) => runtime.options.appendSystemPrompt ?? "" });
107
+ registerSlot({ name: "date", description: "Current date.", options: { includeTime: INCLUDE_TIME_OPTION }, render: renderDate });
108
+ registerSlot({ name: "cwd", description: "Current working directory.", render: ({ runtime }) => renderCwd(runtime) });
109
+ registerSlot({ name: "date-cwd", description: "Current date and working directory.", options: { includeTime: INCLUDE_TIME_OPTION }, render: (context) => [renderDate(context), renderCwd(context.runtime)].join("\n") });
110
+ registerSlot({ name: "active-model", description: "Current model provider/id.", render: ({ runtime }) => renderActiveModel(runtime) });
111
+ registerSlot({ name: "pi-docs", description: "Pi documentation guidance.", render: () => renderPiDocsGuidance() });
112
+ registerSlot({
113
+ name: "variables",
114
+ description: "Static, session, and turn variables.",
115
+ options: {
116
+ format: FORMAT_OPTION,
117
+ includeStatic: { type: "boolean", default: true, description: "Include static stack variables." },
118
+ includeSession: { type: "boolean", default: true, description: "Include session variables." },
119
+ includeTurn: { type: "boolean", default: true, description: "Include turn variables." },
120
+ },
121
+ render: renderVariables,
122
+ });
123
+ registeringBuiltInSlots = false;
124
+
125
+ export function renderSlotText(
126
+ item: PromptStackSlotItem,
127
+ stack: PromptStack,
128
+ runtime: PromptRuntime,
129
+ diagnostics: PromptStackDiagnostic[],
130
+ ): string {
131
+ const definition = SLOT_RENDERERS.get(item.slot);
132
+ if (!definition) {
133
+ diagnostics.push({ level: "warning", message: `Unsupported slot: ${item.slot}`, itemId: item.id });
134
+ return "";
135
+ }
136
+
137
+ try {
138
+ return definition.render(createSlotRenderContext(item, stack, runtime, diagnostics)) ?? "";
139
+ } catch (error) {
140
+ const detail = error instanceof Error ? error.message : String(error);
141
+ diagnostics.push({ level: "error", message: `Slot "${item.slot}" failed: ${detail}`, itemId: item.id });
142
+ return "";
143
+ }
144
+ }
145
+
146
+ export function registerSlot(definition: PromptSlotDefinition): () => void {
147
+ assertRegistryName("Slot", definition.name);
148
+ if (SLOT_RENDERERS.has(definition.name)) {
149
+ if (registeringBuiltInSlots) return () => {};
150
+ throw new Error(`Slot is already registered: ${definition.name}`);
151
+ }
152
+ SLOT_RENDERERS.set(definition.name, definition);
153
+ SUPPORTED_SLOTS.add(definition.name);
154
+ return () => {
155
+ if (SLOT_RENDERERS.get(definition.name) === definition) {
156
+ SLOT_RENDERERS.delete(definition.name);
157
+ SUPPORTED_SLOTS.delete(definition.name);
158
+ }
159
+ };
160
+ }
161
+
162
+ export function getRegisteredSlots(): readonly PromptSlotDefinition[] {
163
+ return [...SLOT_RENDERERS.values()];
164
+ }
165
+
166
+ function createSlotRenderContext(
167
+ item: PromptStackSlotItem,
168
+ stack: PromptStack,
169
+ runtime: PromptRuntime,
170
+ diagnostics: PromptStackDiagnostic[],
171
+ ): PromptSlotRenderContext {
172
+ return {
173
+ item,
174
+ stack,
175
+ runtime,
176
+ diagnostics,
177
+ options: item.options ?? {},
178
+ helpers: promptRenderHelpers,
179
+ variables: createVariableAccess(runtime, stack),
180
+ format: (options) => promptRenderHelpers.slotTextFormat(item, options),
181
+ };
182
+ }
183
+
184
+ function renderTools({ item, stack, runtime, format, helpers }: PromptSlotRenderContext): string {
185
+ const snippets = runtime.options.toolSnippets ?? {};
186
+ const tools = item.options?.onlyWithSnippets === true
187
+ ? selectedToolNames(stack, runtime).filter((name) => !!snippets[name])
188
+ : selectedToolNames(stack, runtime);
189
+
190
+ if (format() === "plain") {
191
+ const lines = ["Available tools:"];
192
+ if (tools.length === 0) {
193
+ lines.push(item.options?.onlyWithSnippets === true ? "(none)" : "- (none)");
194
+ } else {
195
+ for (const name of tools) {
196
+ lines.push(helpers.plainBullet(name, snippets[name] ?? "No prompt snippet provided."));
197
+ }
198
+ }
199
+ return lines.join("\n");
200
+ }
201
+
202
+ const lines = ["<available_tools>"];
203
+
204
+ if (tools.length === 0) {
205
+ lines.push(" (none)");
206
+ } else {
207
+ for (const name of tools) {
208
+ const snippet = snippets[name] ?? "No prompt snippet provided.";
209
+ lines.push(` <tool name=\"${helpers.escapeXml(name)}\">${helpers.escapeXml(snippet)}</tool>`);
210
+ }
211
+ }
212
+
213
+ lines.push("</available_tools>");
214
+ return lines.join("\n");
215
+ }
216
+
217
+ function renderToolGuidelines({ item, stack, runtime, format, helpers }: PromptSlotRenderContext): string {
218
+ const tools = selectedToolNames(stack, runtime);
219
+ const guidelines: string[] = [];
220
+ const seen = new Set<string>();
221
+ const add = (line: string) => {
222
+ const normalized = line.trim();
223
+ if (!normalized || seen.has(normalized)) return;
224
+ seen.add(normalized);
225
+ guidelines.push(normalized);
226
+ };
227
+
228
+ if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
229
+ add(item.options?.piStyle === true ? "Use bash for file operations like ls, rg, find" : "Use bash for file operations like ls, rg, find.");
230
+ }
231
+
232
+ for (const guideline of runtime.options.promptGuidelines ?? []) add(guideline);
233
+ if (item.options?.includePiDefaultGuidelines === true) {
234
+ add("Be concise in your responses");
235
+ add("Show file paths clearly when working with files");
236
+ }
237
+
238
+ if (guidelines.length === 0) return "";
239
+ if (format() === "plain") {
240
+ const heading = typeof item.options?.heading === "string" ? item.options.heading.trim() : "Tool guidelines:";
241
+ return [
242
+ ...(heading ? [heading] : []),
243
+ ...guidelines.map((line) => `- ${helpers.plainContinuation(line, " ")}`),
244
+ ].join("\n");
245
+ }
246
+ return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
247
+ }
248
+
249
+ function renderSkills({ item, stack, runtime, format, helpers }: PromptSlotRenderContext): string {
250
+ if (item.options?.requireReadTool === true && !selectedToolNames(stack, runtime).includes("read")) return "";
251
+ const skills = (runtime.options.skills ?? [])
252
+ .filter((skill) => !skill.disableModelInvocation)
253
+ .filter((skill) => applyResourcePolicy([skill.name], stack.skills).length > 0);
254
+ if (skills.length === 0) return "";
255
+
256
+ const lines = [
257
+ "The following skills provide specialized instructions for specific tasks.",
258
+ "Use the read tool to load a skill's file when the task matches its description.",
259
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
260
+ "",
261
+ ];
262
+
263
+ if (format() === "plain") {
264
+ lines.push("Available skills:");
265
+ for (const skill of skills) {
266
+ lines.push(helpers.plainBullet(skill.name, skill.description));
267
+ lines.push(` Location: ${helpers.plainContinuation(skill.filePath, " ")}`);
268
+ }
269
+ return lines.join("\n");
270
+ }
271
+
272
+ lines.push("<available_skills>");
273
+ for (const skill of skills) {
274
+ lines.push(" <skill>");
275
+ lines.push(` <name>${helpers.escapeXml(skill.name)}</name>`);
276
+ lines.push(` <description>${helpers.escapeXml(skill.description)}</description>`);
277
+ lines.push(` <location>${helpers.escapeXml(skill.filePath)}</location>`);
278
+ lines.push(" </skill>");
279
+ }
280
+
281
+ lines.push("</available_skills>");
282
+ return lines.join("\n");
283
+ }
284
+
285
+ function renderProjectContext({ item, runtime, format, helpers }: PromptSlotRenderContext): string {
286
+ const contextFiles = runtime.options.contextFiles ?? [];
287
+ if (contextFiles.length === 0) return "";
288
+
289
+ if (format() === "plain") {
290
+ const lines = ["Project context:", "", "Project-specific instructions and guidelines:", ""];
291
+ for (const file of contextFiles) {
292
+ lines.push(`Path: ${file.path}`);
293
+ lines.push(helpers.indentPlainBlock(file.content, " "), "");
294
+ }
295
+ return lines.join("\n").trimEnd();
296
+ }
297
+
298
+ const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
299
+ for (const file of contextFiles) {
300
+ lines.push(`<project_instructions path=\"${helpers.escapeXml(file.path)}\">`);
301
+ lines.push(file.content);
302
+ lines.push("</project_instructions>", "");
303
+ }
304
+ lines.push("</project_context>");
305
+ return lines.join("\n");
306
+ }
307
+
308
+ function renderDate({ item, runtime, helpers }: PromptSlotRenderContext): string {
309
+ const lines = [`Current date: ${helpers.formatDate(runtime.now)}`];
310
+ if (item.options?.includeTime === true) {
311
+ lines.push(`Current time: ${helpers.formatTime(runtime.now)}`);
312
+ }
313
+ return lines.join("\n");
314
+ }
315
+
316
+ function renderCwd(runtime: PromptRuntime): string {
317
+ return `Current working directory: ${promptRenderHelpers.normalizePath(runtime.options.cwd)}`;
318
+ }
319
+
320
+ function renderActiveModel(runtime: PromptRuntime): string {
321
+ const model = runtime.ctx?.model;
322
+ return model ? `Current model: ${model.provider}/${model.id}` : "Current model: (none)";
323
+ }
324
+
325
+ function renderPiDocsGuidance(): string {
326
+ const paths = piDocsPaths();
327
+ return [
328
+ "Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
329
+ `- Main documentation: ${paths.readme}`,
330
+ `- Additional docs: ${paths.docs}`,
331
+ `- Examples: ${paths.examples} (extensions, custom tools, SDK)`,
332
+ "- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
333
+ "- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)",
334
+ "- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing",
335
+ "- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)",
336
+ ].join("\n");
337
+ }
338
+
339
+ function piDocsPaths(): { readme: string; docs: string; examples: string } {
340
+ const fallbackRoot = "@earendil-works/pi-coding-agent";
341
+ try {
342
+ const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;
343
+ if (!resolve) throw new Error("import.meta.resolve unavailable");
344
+ const resolved = resolve("@earendil-works/pi-coding-agent");
345
+ if (!resolved.startsWith("file:")) throw new Error("non-file package resolution");
346
+ const packageRoot = dirname(dirname(fileURLToPath(resolved)));
347
+ return {
348
+ readme: join(packageRoot, "README.md"),
349
+ docs: join(packageRoot, "docs"),
350
+ examples: join(packageRoot, "examples"),
351
+ };
352
+ } catch {
353
+ return {
354
+ readme: `${fallbackRoot}/README.md`,
355
+ docs: `${fallbackRoot}/docs`,
356
+ examples: `${fallbackRoot}/examples`,
357
+ };
358
+ }
359
+ }
360
+
361
+ function renderVariables({ stack, runtime, options, format, variables, helpers }: PromptSlotRenderContext): string {
362
+ const scopes = selectedVariableScopes(options);
363
+ const store = runtime.variables;
364
+ const grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>> = {
365
+ static: {},
366
+ session: {},
367
+ turn: {},
368
+ };
369
+
370
+ if (scopes.includes("static")) grouped.static = collectStaticVariables(stack);
371
+ if (scopes.includes("session")) grouped.session = { ...(store?.session ?? {}) };
372
+ if (scopes.includes("turn")) grouped.turn = { ...(store?.turn ?? {}) };
373
+
374
+ const hasVariables = Object.values(grouped).some((values) => Object.keys(values).length > 0);
375
+ if (!hasVariables) return "";
376
+
377
+ if (format() === "plain") {
378
+ return renderPlainVariables(grouped, variables, helpers);
379
+ }
380
+
381
+ const parts: string[] = ["<variables>"];
382
+
383
+ for (const scope of scopes) {
384
+ const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
385
+ if (entries.length === 0) continue;
386
+ parts.push(` <${scope}>`);
387
+ for (const [name, value] of entries) {
388
+ parts.push(` <var name=\"${helpers.escapeXml(name)}\">${helpers.escapeXml(variables.toPromptText(value))}</var>`);
389
+ }
390
+ parts.push(` </${scope}>`);
391
+ }
392
+
393
+ parts.push("</variables>");
394
+ return parts.join("\n");
395
+ }
396
+
397
+ function renderPlainVariables(
398
+ grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>>,
399
+ variables: PromptVariableAccess,
400
+ helpers: PromptRenderHelpers,
401
+ ): string {
402
+ const parts: string[] = ["Variables:"];
403
+
404
+ for (const scope of ["static", "session", "turn"] as const) {
405
+ const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
406
+ if (entries.length === 0) continue;
407
+ parts.push(`${scope}:`);
408
+ for (const [name, value] of entries) {
409
+ parts.push(helpers.plainBullet(name, variables.toPromptText(value)));
410
+ }
411
+ }
412
+
413
+ return parts.join("\n");
414
+ }
package/src/storage.ts CHANGED
@@ -1,9 +1,26 @@
1
+ import { homedir } from "node:os";
1
2
  import { isAbsolute, join, relative, resolve } from "node:path";
2
3
 
3
4
  export function promptStacksDir(cwd: string): string {
4
5
  return join(cwd, ".pi", "forge", "prompt-stacks");
5
6
  }
6
7
 
8
+ export function forgeDir(cwd: string): string {
9
+ return join(cwd, ".pi", "forge");
10
+ }
11
+
12
+ export function forgeExtensionsDir(cwd: string): string {
13
+ return join(forgeDir(cwd), "extensions");
14
+ }
15
+
16
+ export function globalForgeDir(): string {
17
+ return join(homedir(), ".pi", "forge");
18
+ }
19
+
20
+ export function globalForgeExtensionsDir(): string {
21
+ return join(globalForgeDir(), "extensions");
22
+ }
23
+
7
24
  export function legacyPromptStacksDir(cwd: string): string {
8
25
  return join(cwd, ".pi", "prompt-stacks");
9
26
  }
package/src/types.ts CHANGED
@@ -112,6 +112,8 @@ export interface PromptStackSlotOptions {
112
112
  format?: PromptStackSlotFormat;
113
113
  /** For tools: only render tools that provide prompt snippets, matching Pi's default prompt builder. */
114
114
  onlyWithSnippets?: boolean;
115
+ /** For date/date-cwd: include the exact current time. Default: false. */
116
+ includeTime?: boolean;
115
117
  /** For tool-guidelines: override the plain-format section heading. */
116
118
  heading?: string;
117
119
  /** For tool-guidelines: include Pi's default concise/file-path guideline bullets. */
@@ -204,17 +206,4 @@ export interface CompileMessagesResult {
204
206
  diagnostics: PromptStackDiagnostic[];
205
207
  }
206
208
 
207
- export const SUPPORTED_SLOTS = new Set<PromptStackSlot>([
208
- "chat-history",
209
- "tools",
210
- "tool-guidelines",
211
- "skills",
212
- "project-context",
213
- "append-system-prompt",
214
- "date",
215
- "cwd",
216
- "date-cwd",
217
- "active-model",
218
- "pi-docs",
219
- "variables",
220
- ]);
209
+ export { SUPPORTED_SLOTS } from "./slot-renderers.ts";