@zihanw/pi-forge 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/PUBLIC_API.md +5 -0
  3. package/README.md +93 -478
  4. package/README.zh-CN.md +88 -461
  5. package/SUBAGENT_ADAPTER_CONTRACT.md +5 -0
  6. package/dist/agent-profile.d.ts +71 -0
  7. package/dist/agent-profile.d.ts.map +1 -0
  8. package/dist/agent-profile.js +303 -0
  9. package/dist/agent-profile.js.map +1 -0
  10. package/dist/forge-config.d.ts +88 -0
  11. package/dist/forge-config.d.ts.map +1 -0
  12. package/dist/forge-config.js +290 -0
  13. package/dist/forge-config.js.map +1 -0
  14. package/dist/forge-extensions.d.ts.map +1 -1
  15. package/dist/forge-extensions.js +19 -3
  16. package/dist/forge-extensions.js.map +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +97 -358
  20. package/dist/index.js.map +1 -1
  21. package/dist/lifecycle.d.ts +11 -3
  22. package/dist/lifecycle.d.ts.map +1 -1
  23. package/dist/lifecycle.js +90 -4
  24. package/dist/lifecycle.js.map +1 -1
  25. package/dist/loader.d.ts.map +1 -1
  26. package/dist/loader.js +116 -5
  27. package/dist/loader.js.map +1 -1
  28. package/dist/payload-capture.d.ts.map +1 -1
  29. package/dist/payload-capture.js +27 -0
  30. package/dist/payload-capture.js.map +1 -1
  31. package/dist/payload-command.d.ts +3 -3
  32. package/dist/payload-command.d.ts.map +1 -1
  33. package/dist/payload-command.js.map +1 -1
  34. package/dist/preview.d.ts +2 -2
  35. package/dist/preview.d.ts.map +1 -1
  36. package/dist/preview.js +2 -3
  37. package/dist/preview.js.map +1 -1
  38. package/dist/profile-command.d.ts +12 -0
  39. package/dist/profile-command.d.ts.map +1 -0
  40. package/dist/profile-command.js +291 -0
  41. package/dist/profile-command.js.map +1 -0
  42. package/dist/profile-service.d.ts +103 -0
  43. package/dist/profile-service.d.ts.map +1 -0
  44. package/dist/profile-service.js +220 -0
  45. package/dist/profile-service.js.map +1 -0
  46. package/dist/runtime/profile-runtime.d.ts +13 -0
  47. package/dist/runtime/profile-runtime.d.ts.map +1 -0
  48. package/dist/runtime/profile-runtime.js +47 -0
  49. package/dist/runtime/profile-runtime.js.map +1 -0
  50. package/dist/runtime/prompt-stack-runtime.d.ts +22 -0
  51. package/dist/runtime/prompt-stack-runtime.d.ts.map +1 -0
  52. package/dist/runtime/prompt-stack-runtime.js +104 -0
  53. package/dist/runtime/prompt-stack-runtime.js.map +1 -0
  54. package/dist/runtime/subagent-runtime.d.ts +45 -0
  55. package/dist/runtime/subagent-runtime.d.ts.map +1 -0
  56. package/dist/runtime/subagent-runtime.js +335 -0
  57. package/dist/runtime/subagent-runtime.js.map +1 -0
  58. package/dist/runtime/tool-policy-runtime.d.ts +15 -0
  59. package/dist/runtime/tool-policy-runtime.d.ts.map +1 -0
  60. package/dist/runtime/tool-policy-runtime.js +170 -0
  61. package/dist/runtime/tool-policy-runtime.js.map +1 -0
  62. package/dist/runtime/web-editor-runtime.d.ts +9 -0
  63. package/dist/runtime/web-editor-runtime.d.ts.map +1 -0
  64. package/dist/runtime/web-editor-runtime.js +131 -0
  65. package/dist/runtime/web-editor-runtime.js.map +1 -0
  66. package/dist/runtime-state.d.ts +4 -0
  67. package/dist/runtime-state.d.ts.map +1 -1
  68. package/dist/runtime-state.js +2 -0
  69. package/dist/runtime-state.js.map +1 -1
  70. package/dist/storage.d.ts +4 -0
  71. package/dist/storage.d.ts.map +1 -1
  72. package/dist/storage.js +24 -1
  73. package/dist/storage.js.map +1 -1
  74. package/dist/subagent/canonical.d.ts +22 -0
  75. package/dist/subagent/canonical.d.ts.map +1 -0
  76. package/dist/subagent/canonical.js +24 -0
  77. package/dist/subagent/canonical.js.map +1 -0
  78. package/dist/subagent/context.d.ts +8 -0
  79. package/dist/subagent/context.d.ts.map +1 -0
  80. package/dist/subagent/context.js +125 -0
  81. package/dist/subagent/context.js.map +1 -0
  82. package/dist/subagent/contract.d.ts +10 -0
  83. package/dist/subagent/contract.d.ts.map +1 -0
  84. package/dist/subagent/contract.js +10 -0
  85. package/dist/subagent/contract.js.map +1 -0
  86. package/dist/subagent/index.d.ts +14 -0
  87. package/dist/subagent/index.d.ts.map +1 -0
  88. package/dist/subagent/index.js +14 -0
  89. package/dist/subagent/index.js.map +1 -0
  90. package/dist/subagent/plan.d.ts +18 -0
  91. package/dist/subagent/plan.d.ts.map +1 -0
  92. package/dist/subagent/plan.js +157 -0
  93. package/dist/subagent/plan.js.map +1 -0
  94. package/dist/subagent/preflight.d.ts +4 -0
  95. package/dist/subagent/preflight.d.ts.map +1 -0
  96. package/dist/subagent/preflight.js +108 -0
  97. package/dist/subagent/preflight.js.map +1 -0
  98. package/dist/subagent/request.d.ts +4 -0
  99. package/dist/subagent/request.d.ts.map +1 -0
  100. package/dist/subagent/request.js +122 -0
  101. package/dist/subagent/request.js.map +1 -0
  102. package/dist/subagent/response.d.ts +8 -0
  103. package/dist/subagent/response.d.ts.map +1 -0
  104. package/dist/subagent/response.js +155 -0
  105. package/dist/subagent/response.js.map +1 -0
  106. package/dist/subagent/tools.d.ts +4 -0
  107. package/dist/subagent/tools.d.ts.map +1 -0
  108. package/dist/subagent/tools.js +42 -0
  109. package/dist/subagent/tools.js.map +1 -0
  110. package/dist/subagent/types.d.ts +268 -0
  111. package/dist/subagent/types.d.ts.map +1 -0
  112. package/dist/subagent/types.js +3 -0
  113. package/dist/subagent/types.js.map +1 -0
  114. package/dist/subagent/validation.d.ts +35 -0
  115. package/dist/subagent/validation.d.ts.map +1 -0
  116. package/dist/subagent/validation.js +314 -0
  117. package/dist/subagent/validation.js.map +1 -0
  118. package/dist/subagent-command.d.ts +4 -0
  119. package/dist/subagent-command.d.ts.map +1 -0
  120. package/dist/subagent-command.js +246 -0
  121. package/dist/subagent-command.js.map +1 -0
  122. package/dist/subagent-host.d.ts +44 -0
  123. package/dist/subagent-host.d.ts.map +1 -0
  124. package/dist/subagent-host.js +292 -0
  125. package/dist/subagent-host.js.map +1 -0
  126. package/dist/subagent-profile-tool.d.ts +49 -0
  127. package/dist/subagent-profile-tool.d.ts.map +1 -0
  128. package/dist/subagent-profile-tool.js +124 -0
  129. package/dist/subagent-profile-tool.js.map +1 -0
  130. package/dist/subagent-tool.d.ts +53 -0
  131. package/dist/subagent-tool.d.ts.map +1 -0
  132. package/dist/subagent-tool.js +456 -0
  133. package/dist/subagent-tool.js.map +1 -0
  134. package/dist/web-editor/client-script.d.ts +2 -0
  135. package/dist/web-editor/client-script.d.ts.map +1 -0
  136. package/dist/web-editor/client-script.generated.d.ts +2 -0
  137. package/dist/web-editor/client-script.generated.d.ts.map +1 -0
  138. package/dist/web-editor/client-script.generated.js +3 -0
  139. package/dist/web-editor/client-script.generated.js.map +1 -0
  140. package/dist/web-editor/client-script.js +2 -0
  141. package/dist/web-editor/client-script.js.map +1 -0
  142. package/dist/web-editor/client-styles.d.ts +2 -0
  143. package/dist/web-editor/client-styles.d.ts.map +1 -0
  144. package/dist/web-editor/client-styles.generated.d.ts +2 -0
  145. package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
  146. package/dist/web-editor/client-styles.generated.js +3 -0
  147. package/dist/web-editor/client-styles.generated.js.map +1 -0
  148. package/dist/web-editor/client-styles.js +2 -0
  149. package/dist/web-editor/client-styles.js.map +1 -0
  150. package/dist/web-editor/page.d.ts +2 -0
  151. package/dist/web-editor/page.d.ts.map +1 -1
  152. package/dist/web-editor/page.js +14 -3319
  153. package/dist/web-editor/page.js.map +1 -1
  154. package/dist/web-editor/server.d.ts.map +1 -1
  155. package/dist/web-editor/server.js +148 -0
  156. package/dist/web-editor/server.js.map +1 -1
  157. package/dist/web-editor/styles.d.ts +2 -0
  158. package/dist/web-editor/styles.d.ts.map +1 -0
  159. package/dist/web-editor/styles.js +1053 -0
  160. package/dist/web-editor/styles.js.map +1 -0
  161. package/dist/web-editor/types.d.ts +79 -0
  162. package/dist/web-editor/types.d.ts.map +1 -1
  163. package/dist/web-host.d.ts +16 -5
  164. package/dist/web-host.d.ts.map +1 -1
  165. package/dist/web-host.js +307 -0
  166. package/dist/web-host.js.map +1 -1
  167. package/docs/README.md +41 -0
  168. package/docs/concepts/agent-profiles.md +60 -0
  169. package/docs/concepts/prompt-stacks.md +90 -0
  170. package/docs/design/README.md +17 -0
  171. package/docs/design/roadmap-0.4-archive.md +216 -0
  172. package/docs/design/subagents/design-review.md +220 -0
  173. package/docs/design/subagents/interface-design.md +274 -0
  174. package/docs/design/subagents/sdk-spike-findings.md +117 -0
  175. package/docs/development/complexity-review.md +86 -0
  176. package/docs/development/release.md +31 -0
  177. package/docs/development/roadmap.md +42 -0
  178. package/docs/development/setup.md +75 -0
  179. package/docs/getting-started.md +93 -0
  180. package/docs/guides/custom-macros-and-slots.md +68 -0
  181. package/docs/guides/debugging.md +39 -0
  182. package/docs/guides/delegation.md +99 -0
  183. package/docs/guides/sillytavern-import.md +47 -0
  184. package/docs/guides/use-cases.md +65 -0
  185. package/docs/guides/web-editor.md +75 -0
  186. package/docs/reference/commands.md +60 -0
  187. package/docs/reference/configuration.md +64 -0
  188. package/docs/reference/features.md +279 -0
  189. package/docs/reference/macros-and-slots.md +82 -0
  190. package/docs/reference/public-api.md +28 -0
  191. package/docs/reference/stack-schema.md +167 -0
  192. package/docs/reference/subagent-adapter.md +204 -0
  193. package/docs/zh-CN/README.md +37 -0
  194. package/docs/zh-CN/concepts/agent-profiles.md +44 -0
  195. package/docs/zh-CN/concepts/prompt-stacks.md +40 -0
  196. package/docs/zh-CN/getting-started.md +79 -0
  197. package/docs/zh-CN/guides/delegation.md +66 -0
  198. package/docs/zh-CN/guides/web-editor.md +45 -0
  199. package/docs/zh-CN/reference/commands.md +58 -0
  200. package/package.json +55 -13
  201. package/src/compiler.ts +0 -578
  202. package/src/extension-registry.ts +0 -33
  203. package/src/forge-extensions.ts +0 -223
  204. package/src/index.ts +0 -445
  205. package/src/lifecycle.ts +0 -171
  206. package/src/loader.ts +0 -394
  207. package/src/macro-engine.ts +0 -358
  208. package/src/payload-capture.ts +0 -85
  209. package/src/payload-command.ts +0 -138
  210. package/src/policy.ts +0 -42
  211. package/src/preset-command.ts +0 -280
  212. package/src/preview.ts +0 -226
  213. package/src/regex.ts +0 -500
  214. package/src/render-helpers.ts +0 -169
  215. package/src/runtime-state.ts +0 -40
  216. package/src/sillytavern-importer/items.ts +0 -98
  217. package/src/sillytavern-importer/macros.ts +0 -159
  218. package/src/sillytavern-importer/prompt-order.ts +0 -54
  219. package/src/sillytavern-importer/regex.ts +0 -270
  220. package/src/sillytavern-importer/report.ts +0 -202
  221. package/src/sillytavern-importer/types.ts +0 -120
  222. package/src/sillytavern-importer.ts +0 -152
  223. package/src/slot-renderers.ts +0 -414
  224. package/src/stack-migration.ts +0 -159
  225. package/src/storage.ts +0 -45
  226. package/src/types.ts +0 -209
  227. package/src/web-editor/index.ts +0 -2
  228. package/src/web-editor/page.ts +0 -3330
  229. package/src/web-editor/server.ts +0 -294
  230. package/src/web-editor/types.ts +0 -98
  231. package/src/web-host.ts +0 -232
@@ -1,33 +0,0 @@
1
- export type PromptExtensionOptionType = "boolean" | "string" | "number" | "enum" | "stringArray";
2
-
3
- export interface PromptExtensionOptionDefinition {
4
- type: PromptExtensionOptionType;
5
- description?: string;
6
- default?: string | number | boolean | readonly string[];
7
- values?: readonly string[];
8
- min?: number;
9
- max?: number;
10
- integer?: boolean;
11
- }
12
-
13
- export type PromptExtensionOptionsSchema = Record<string, PromptExtensionOptionDefinition>;
14
-
15
- export interface PromptExtensionArgumentDefinition {
16
- name: string;
17
- description?: string;
18
- required?: boolean;
19
- variadic?: boolean;
20
- expansion?: "eager" | "lazy";
21
- }
22
-
23
- export interface PromptRegistryEntry {
24
- name: string;
25
- description?: string;
26
- source?: string;
27
- }
28
-
29
- export function assertRegistryName(kind: string, name: string): void {
30
- if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(name)) {
31
- throw new Error(`${kind} name must start with a letter and contain only letters, numbers, underscore, dot, or hyphen: ${name}`);
32
- }
33
- }
@@ -1,223 +0,0 @@
1
- import { existsSync, readdirSync, statSync } from "node:fs";
2
- import { basename, join } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import {
5
- getRegisteredMacros,
6
- registerMacro,
7
- type PromptMacroDefinition,
8
- } from "./macro-engine.ts";
9
- import { promptRenderHelpers, type PromptRenderHelpers } from "./render-helpers.ts";
10
- import {
11
- getRegisteredSlots,
12
- registerSlot,
13
- type PromptSlotDefinition,
14
- } from "./slot-renderers.ts";
15
- import { forgeDir, forgeExtensionsDir, globalForgeDir, globalForgeExtensionsDir } from "./storage.ts";
16
- import type { PromptStackDiagnostic } from "./types.ts";
17
-
18
- export interface ForgeExtensionApi {
19
- cwd: string;
20
- forgeDir: string;
21
- extensionPath: string;
22
- helpers: PromptRenderHelpers;
23
- registerMacro(definition: PromptMacroDefinition): () => void;
24
- registerSlot(definition: PromptSlotDefinition): () => void;
25
- getRegisteredMacros: typeof getRegisteredMacros;
26
- getRegisteredSlots: typeof getRegisteredSlots;
27
- }
28
-
29
- export type ForgeExtensionRegister = (api: ForgeExtensionApi) => void | (() => void) | Promise<void | (() => void)>;
30
-
31
- export interface ForgeExtensionState {
32
- unregister: Array<() => void>;
33
- loadVersion: number;
34
- }
35
-
36
- export interface ForgeExtensionLoadResult {
37
- diagnostics: PromptStackDiagnostic[];
38
- loadedPaths: string[];
39
- }
40
-
41
- interface ForgeExtensionCandidate {
42
- filePath: string;
43
- forgeDir: string;
44
- }
45
-
46
- export function createForgeExtensionState(): ForgeExtensionState {
47
- return { unregister: [], loadVersion: 0 };
48
- }
49
-
50
- export async function reloadForgeExtensions(cwd: string, state: ForgeExtensionState): Promise<ForgeExtensionLoadResult> {
51
- const diagnostics = unloadForgeExtensions(state);
52
- state.loadVersion++;
53
- const loadedPaths: string[] = [];
54
-
55
- for (const candidate of discoverForgeExtensionCandidates(cwd)) {
56
- const result = await loadForgeExtensionFile(cwd, candidate.forgeDir, candidate.filePath, state.loadVersion);
57
- diagnostics.push(...result.diagnostics);
58
- if (result.loaded) {
59
- state.unregister.push(...result.unregister);
60
- loadedPaths.push(candidate.filePath);
61
- }
62
- }
63
-
64
- return { diagnostics, loadedPaths };
65
- }
66
-
67
- export function unloadForgeExtensions(state: ForgeExtensionState): PromptStackDiagnostic[] {
68
- const diagnostics: PromptStackDiagnostic[] = [];
69
- for (const unregister of state.unregister.splice(0).reverse()) {
70
- try {
71
- unregister();
72
- } catch (error) {
73
- diagnostics.push({
74
- level: "warning",
75
- message: `Failed to unload pi-forge extension: ${error instanceof Error ? error.message : String(error)}`,
76
- });
77
- }
78
- }
79
- return diagnostics;
80
- }
81
-
82
- export function discoverForgeExtensionFiles(cwd: string): string[] {
83
- return discoverForgeExtensionCandidates(cwd).map((candidate) => candidate.filePath);
84
- }
85
-
86
- function discoverForgeExtensionCandidates(cwd: string): ForgeExtensionCandidate[] {
87
- const dirs = [
88
- { dir: globalForgeExtensionsDir(), forgeDir: globalForgeDir() },
89
- { dir: forgeExtensionsDir(cwd), forgeDir: forgeDir(cwd) },
90
- ];
91
- const seenDirs = new Set<string>();
92
- return dirs.flatMap(({ dir, forgeDir }) => {
93
- if (seenDirs.has(dir)) return [];
94
- seenDirs.add(dir);
95
- return discoverForgeExtensionFilesInDir(dir).map((filePath) => ({ filePath, forgeDir }));
96
- });
97
- }
98
-
99
- function discoverForgeExtensionFilesInDir(dir: string): string[] {
100
- if (!existsSync(dir)) return [];
101
-
102
- const files: string[] = [];
103
- let entries: string[];
104
- try {
105
- entries = readdirSync(dir);
106
- } catch {
107
- return [];
108
- }
109
-
110
- for (const name of entries.sort()) {
111
- if (name.startsWith(".")) continue;
112
- const fullPath = join(dir, name);
113
- let stats;
114
- try {
115
- stats = statSync(fullPath);
116
- } catch {
117
- continue;
118
- }
119
-
120
- if (stats.isFile() && isForgeExtensionFile(fullPath)) {
121
- files.push(fullPath);
122
- continue;
123
- }
124
-
125
- if (!stats.isDirectory()) continue;
126
- for (const indexName of ["index.ts", "index.js", "index.mjs", "index.cjs"]) {
127
- const indexPath = join(fullPath, indexName);
128
- if (existsSync(indexPath)) {
129
- files.push(indexPath);
130
- break;
131
- }
132
- }
133
- }
134
-
135
- return files;
136
- }
137
-
138
- async function loadForgeExtensionFile(
139
- cwd: string,
140
- forgeDirPath: string,
141
- filePath: string,
142
- loadVersion: number,
143
- ): Promise<{ loaded: boolean; unregister: Array<() => void>; diagnostics: PromptStackDiagnostic[] }> {
144
- const unregister: Array<() => void> = [];
145
- const diagnostics: PromptStackDiagnostic[] = [];
146
- try {
147
- const module = await import(`${pathToFileURL(filePath).href}?piForgeReload=${loadVersion}`);
148
- const register = readRegisterFunction(module);
149
- if (!register) {
150
- return {
151
- loaded: false,
152
- unregister,
153
- diagnostics: [{
154
- level: "warning",
155
- message: `pi-forge extension ${extensionLabel(filePath)} must export a default function or named register function.`,
156
- }],
157
- };
158
- }
159
-
160
- const cleanup = await register(createForgeExtensionApi(cwd, forgeDirPath, filePath, unregister));
161
- if (typeof cleanup === "function") unregister.push(once(cleanup));
162
- return { loaded: true, unregister, diagnostics };
163
- } catch (error) {
164
- for (const cleanup of unregister.splice(0).reverse()) {
165
- try {
166
- cleanup();
167
- } catch {
168
- // Preserve the original load error; cleanup failures are secondary.
169
- }
170
- }
171
- diagnostics.push({
172
- level: "warning",
173
- message: `Failed to load pi-forge extension ${extensionLabel(filePath)}: ${error instanceof Error ? error.message : String(error)}`,
174
- });
175
- return { loaded: false, unregister: [], diagnostics };
176
- }
177
- }
178
-
179
- function createForgeExtensionApi(cwd: string, forgeDirPath: string, extensionPath: string, unregister: Array<() => void>): ForgeExtensionApi {
180
- return {
181
- cwd,
182
- forgeDir: forgeDirPath,
183
- extensionPath,
184
- helpers: promptRenderHelpers,
185
- registerMacro: (definition) => trackUnregister(unregister, registerMacro(definition)),
186
- registerSlot: (definition) => trackUnregister(unregister, registerSlot(definition)),
187
- getRegisteredMacros,
188
- getRegisteredSlots,
189
- };
190
- }
191
-
192
- function trackUnregister(unregisters: Array<() => void>, unregister: () => void): () => void {
193
- const tracked = once(unregister);
194
- unregisters.push(tracked);
195
- return tracked;
196
- }
197
-
198
- function once(callback: () => void): () => void {
199
- let active = true;
200
- return () => {
201
- if (!active) return;
202
- active = false;
203
- callback();
204
- };
205
- }
206
-
207
- function readRegisterFunction(module: unknown): ForgeExtensionRegister | undefined {
208
- const record = module && typeof module === "object" ? module as { default?: unknown; register?: unknown } : {};
209
- if (typeof record.default === "function") return record.default as ForgeExtensionRegister;
210
- if (typeof record.register === "function") return record.register as ForgeExtensionRegister;
211
- if (record.default && typeof record.default === "object" && typeof (record.default as { register?: unknown }).register === "function") {
212
- return (record.default as { register: ForgeExtensionRegister }).register;
213
- }
214
- return undefined;
215
- }
216
-
217
- function isForgeExtensionFile(filePath: string): boolean {
218
- return (filePath.endsWith(".ts") || filePath.endsWith(".js") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) && !filePath.endsWith(".d.ts");
219
- }
220
-
221
- function extensionLabel(filePath: string): string {
222
- return basename(filePath);
223
- }
package/src/index.ts DELETED
@@ -1,445 +0,0 @@
1
- import type { BuildSystemPromptOptions, ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { createForgeExtensionState, reloadForgeExtensions, unloadForgeExtensions } from "./forge-extensions.ts";
3
- import { registerLifecycleHandlers } from "./lifecycle.ts";
4
- import { chooseDefaultStack, isDisabledPromptStackId, loadPromptStacks } from "./loader.ts";
5
- import { registerPayloadCommands, registerPayloadRequestHandler, armPayloadIntercept, clearPayloadCapture, webPayloadSnapshot } from "./payload-command.ts";
6
- import { applyResourcePolicy, hasResourcePolicy } from "./policy.ts";
7
- import { buildPreview, showText } from "./preview.ts";
8
- import { registerPresetCommand, selectedActiveId as selectedActiveIdForState } from "./preset-command.ts";
9
- import { createRuntimeState, STATE_ENTRY_TYPE } from "./runtime-state.ts";
10
- import type { PromptStack, PromptStackDiagnostic } from "./types.ts";
11
- import { createWebEditorHost, loadWebEditorSettings, type WebHostRuntime } from "./web-host.ts";
12
- import { startWebEditorServer, type WebEditorPolicyResource, type WebEditorPolicyResources, type WebEditorServer } from "./web-editor/index.ts";
13
-
14
- export {
15
- getRegisteredMacros,
16
- registerMacro,
17
- type PromptMacroDefinition,
18
- type PromptMacroRenderContext,
19
- type PromptMacroRenderer,
20
- } from "./macro-engine.ts";
21
- export {
22
- getRegisteredSlots,
23
- registerSlot,
24
- type PromptSlotDefinition,
25
- type PromptSlotRenderContext,
26
- type PromptSlotRenderer,
27
- } from "./slot-renderers.ts";
28
- export {
29
- type PromptExtensionArgumentDefinition,
30
- type PromptExtensionOptionDefinition,
31
- type PromptExtensionOptionsSchema,
32
- type PromptExtensionOptionType,
33
- type PromptRegistryEntry,
34
- } from "./extension-registry.ts";
35
- export {
36
- type ForgeExtensionApi,
37
- type ForgeExtensionRegister,
38
- } from "./forge-extensions.ts";
39
- export {
40
- createVariableAccess,
41
- promptRenderHelpers,
42
- type PromptRenderHelpers,
43
- type PromptVariableAccess,
44
- type PromptVariableScope,
45
- type PromptWritableVariableScope,
46
- } from "./render-helpers.ts";
47
-
48
- const WEB_EDITOR_GLOBAL_KEY = "__piForgeWebEditor";
49
-
50
- interface SharedWebEditorState {
51
- server?: WebEditorServer;
52
- cwd?: string;
53
- preferredPort?: number;
54
- }
55
-
56
- interface SharedWebEditorRegistry {
57
- byCwd: Record<string, SharedWebEditorState | undefined>;
58
- }
59
-
60
- type PiForgeGlobal = typeof globalThis & {
61
- __piForgeWebEditor?: SharedWebEditorRegistry;
62
- };
63
-
64
- function getSharedWebEditorRegistry(): SharedWebEditorRegistry {
65
- const globalScope = globalThis as PiForgeGlobal;
66
- globalScope[WEB_EDITOR_GLOBAL_KEY] ??= { byCwd: {} };
67
- return globalScope[WEB_EDITOR_GLOBAL_KEY];
68
- }
69
-
70
- export default function piForge(pi: ExtensionAPI) {
71
- const sharedWebEditors = getSharedWebEditorRegistry();
72
- const state = createRuntimeState();
73
- let webEditor: WebEditorServer | undefined;
74
- let webEditorCwd: string | undefined;
75
- let webEditorPreferredPort: number | undefined;
76
- let toolPolicyBaseline: string[] | undefined;
77
- const forgeExtensionState = createForgeExtensionState();
78
-
79
- function activeId(): string | undefined {
80
- return state.active?.stack.id;
81
- }
82
-
83
- function selectedActiveId(): string | undefined {
84
- return selectedActiveIdForState(state);
85
- }
86
-
87
- function persistActiveSelection(id: string): void {
88
- if (id === state.lastPersistedActiveId) return;
89
- pi.appendEntry(STATE_ENTRY_TYPE, { activeStackId: id });
90
- state.lastPersistedActiveId = id;
91
- }
92
-
93
- function setActive(id: string | undefined, ctx?: ExtensionContext): boolean {
94
- if (!id || isDisabledPromptStackId(id)) {
95
- state.active = undefined;
96
- if (id) persistActiveSelection("none");
97
- if (ctx) updateStatus(ctx);
98
- syncActiveToolPolicy(ctx);
99
- return true;
100
- }
101
-
102
- const found = state.stacks.find((candidate) => candidate.stack.id === id);
103
- if (!found) return false;
104
- state.active = found;
105
- persistActiveSelection(found.stack.id);
106
- if (ctx) updateStatus(ctx);
107
- syncActiveToolPolicy(ctx);
108
- return true;
109
- }
110
-
111
- async function reloadStacks(ctx: ExtensionContext, preferredId?: string): Promise<void> {
112
- if (!ctx.isProjectTrusted()) {
113
- const unloadDiagnostics = unloadForgeExtensions(forgeExtensionState);
114
- state.forgeExtensionDiagnostics = unloadDiagnostics;
115
- state.forgeExtensionPaths = [];
116
- state.stacks = [];
117
- state.active = undefined;
118
- syncActiveToolPolicy(ctx);
119
- ctx.ui.notify("pi-forge: project is not trusted; prompt stacks are disabled.", "warning");
120
- updateStatus(ctx);
121
- return;
122
- }
123
-
124
- const extensionResult = await reloadForgeExtensions(ctx.cwd, forgeExtensionState);
125
- state.forgeExtensionDiagnostics = extensionResult.diagnostics;
126
- state.forgeExtensionPaths = extensionResult.loadedPaths;
127
- state.stacks = loadPromptStacks(ctx.cwd);
128
- if (state.forgeExtensionDiagnostics.length > 0) {
129
- for (const loaded of state.stacks) loaded.diagnostics.unshift(...state.forgeExtensionDiagnostics);
130
- }
131
- state.active = chooseDefaultStack(state.stacks, preferredId);
132
- updateStatus(ctx);
133
- syncActiveToolPolicy(ctx);
134
- }
135
-
136
- function updateStatus(ctx: ExtensionContext): void {
137
- if (state.active) {
138
- ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + state.active.stack.id));
139
- } else {
140
- ctx.ui.setStatus("pi-forge", undefined);
141
- state.latestCompileDiagnostics = [];
142
- ctx.ui.setStatus("pi-forge-diagnostics", undefined);
143
- }
144
- }
145
-
146
- function syncActiveToolPolicy(ctx?: ExtensionContext): void {
147
- const policy = state.active?.stack.tools;
148
- if (!hasResourcePolicy(policy)) {
149
- restoreToolPolicy(ctx);
150
- return;
151
- }
152
-
153
- const baseline = toolPolicyBaseline ?? pi.getActiveTools();
154
- toolPolicyBaseline ??= [...baseline];
155
- const nextTools = applyResourcePolicy(filterKnownTools(baseline), policy);
156
- pi.setActiveTools(nextTools);
157
- if (ctx) {
158
- const label = nextTools.length > 0 ? `tools:${nextTools.length}` : "tools:none";
159
- ctx.ui.setStatus("pi-forge-tools", ctx.ui.theme.fg(nextTools.length > 0 ? "accent" : "warning", label));
160
- }
161
- }
162
-
163
- function restoreToolPolicy(ctx?: ExtensionContext): void {
164
- if (toolPolicyBaseline) {
165
- pi.setActiveTools(filterKnownTools(toolPolicyBaseline));
166
- toolPolicyBaseline = undefined;
167
- }
168
- if (ctx) ctx.ui.setStatus("pi-forge-tools", undefined);
169
- }
170
-
171
- function filterKnownTools(names: string[]): string[] {
172
- const known = new Set(pi.getAllTools().map((tool) => tool.name));
173
- if (known.size === 0) return names;
174
- return names.filter((name) => known.has(name));
175
- }
176
-
177
- function notifyActivePreset(ctx: ExtensionContext, detail: string): void {
178
- if (!state.active) return;
179
- const errorCount = state.active.diagnostics.filter((d) => d.level === "error").length;
180
- const warningCount = state.active.diagnostics.filter((d) => d.level === "warning").length;
181
- const suffix = errorCount || warningCount ? " (" + errorCount + " errors, " + warningCount + " warnings)" : "";
182
- ctx.ui.notify("pi-forge: active preset " + state.active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
183
- }
184
-
185
- function recordCompileDiagnostics(ctx: ExtensionContext, diagnostics: PromptStackDiagnostic[]): void {
186
- state.latestCompileDiagnostics = diagnostics;
187
- const errors = diagnostics.filter((d) => d.level === "error").length;
188
- const warnings = diagnostics.filter((d) => d.level === "warning").length;
189
- if (errors || warnings) {
190
- ctx.ui.setStatus("pi-forge-diagnostics", ctx.ui.theme.fg(errors ? "error" : "warning", `forge:${errors}e/${warnings}w`));
191
- return;
192
- }
193
- ctx.ui.setStatus("pi-forge-diagnostics", undefined);
194
- }
195
-
196
- function webHostRuntime(ctx: ExtensionCommandContext): WebHostRuntime {
197
- return {
198
- getStacks: () => state.stacks,
199
- getActive: () => state.active,
200
- getActiveId: activeId,
201
- getSelectedActiveId: selectedActiveId,
202
- setActive: (id) => setActive(id, ctx),
203
- reloadStacks: (preferredId) => reloadStacks(ctx, preferredId),
204
- buildPreview: (target) => buildPreview(ctx, target, state.sessionVariables, previewOptionsForStack(ctx, target.stack)),
205
- getPolicyResources: () => getPolicyResources(ctx),
206
- getPayload: () => ({ ok: true, ...webPayloadSnapshot(state) }),
207
- armPayload: (savePath) => {
208
- armPayloadIntercept(state, ctx, savePath, "web");
209
- return { ok: true, ...webPayloadSnapshot(state) };
210
- },
211
- clearPayload: () => {
212
- clearPayloadCapture(state, ctx);
213
- return { ok: true, ...webPayloadSnapshot(state) };
214
- },
215
- };
216
- }
217
-
218
- function getPolicyResources(ctx: ExtensionCommandContext): WebEditorPolicyResources {
219
- const options = ctx.getSystemPromptOptions();
220
- const activeTools = new Set(pi.getActiveTools());
221
- const snippets = options.toolSnippets ?? {};
222
- const tools = pi.getAllTools()
223
- .map((tool) => normalizeToolResource(tool, activeTools, snippets))
224
- .filter(hasPolicyResourceName)
225
- .sort(comparePolicyResource);
226
- const skills = (options.skills ?? [])
227
- .map(normalizeSkillResource)
228
- .filter(hasPolicyResourceName)
229
- .sort(comparePolicyResource);
230
- return { tools, skills };
231
- }
232
-
233
- function previewOptionsForStack(ctx: ExtensionCommandContext, stack: PromptStack): BuildSystemPromptOptions {
234
- const base = ctx.getSystemPromptOptions();
235
- const baseSelectedTools = Array.isArray(base.selectedTools) ? base.selectedTools : pi.getActiveTools();
236
- const policyActive = hasResourcePolicy(stack.tools);
237
- const baselineTools = policyActive ? (toolPolicyBaseline ?? pi.getActiveTools()) : baseSelectedTools;
238
- const selectedTools = policyActive
239
- ? applyResourcePolicy(filterKnownTools(baselineTools), stack.tools)
240
- : baseSelectedTools;
241
- const selectedToolSet = new Set(selectedTools);
242
- const toolSnippets = filterToolSnippets(base.toolSnippets ?? {}, selectedToolSet);
243
- const toolInfos = pi.getAllTools();
244
- for (const tool of toolInfos) {
245
- const name = stringValue((tool as { name?: unknown }).name);
246
- if (!name || !selectedToolSet.has(name) || toolSnippets[name]) continue;
247
- const snippet = stringValue((tool as { promptSnippet?: unknown }).promptSnippet);
248
- if (snippet) toolSnippets[name] = snippet;
249
- }
250
-
251
- const mappedGuidelines = toolInfos
252
- .filter((tool) => {
253
- const name = stringValue((tool as { name?: unknown }).name);
254
- return !!name && selectedToolSet.has(name);
255
- })
256
- .flatMap((tool) => stringArrayValue((tool as { promptGuidelines?: unknown }).promptGuidelines));
257
- const promptGuidelines = policyActive && !sameStringSet(baseSelectedTools, selectedTools)
258
- ? mappedGuidelines
259
- : (base.promptGuidelines ?? mappedGuidelines);
260
-
261
- return { ...base, selectedTools, toolSnippets, promptGuidelines };
262
- }
263
-
264
- function filterToolSnippets(snippets: Record<string, string | undefined>, selectedTools: Set<string>): Record<string, string> {
265
- const filtered: Record<string, string> = {};
266
- for (const [name, snippet] of Object.entries(snippets)) {
267
- if (selectedTools.has(name) && snippet) filtered[name] = snippet;
268
- }
269
- return filtered;
270
- }
271
-
272
- function normalizeToolResource(
273
- tool: { name?: unknown; description?: unknown; promptSnippet?: unknown; sourceInfo?: unknown },
274
- activeTools: Set<string>,
275
- snippets: Record<string, string | undefined>,
276
- ): WebEditorPolicyResource {
277
- const name = String(tool.name ?? "");
278
- return {
279
- name,
280
- description: stringValue(tool.description) ?? stringValue(tool.promptSnippet) ?? snippets[name],
281
- source: sourceLabel(tool.sourceInfo),
282
- active: activeTools.has(name),
283
- };
284
- }
285
-
286
- function normalizeSkillResource(skill: { name?: unknown; description?: unknown; filePath?: unknown; disableModelInvocation?: unknown }): WebEditorPolicyResource {
287
- return {
288
- name: String(skill.name ?? ""),
289
- description: stringValue(skill.description),
290
- source: stringValue(skill.filePath),
291
- hidden: skill.disableModelInvocation === true,
292
- };
293
- }
294
-
295
- function stringValue(value: unknown): string | undefined {
296
- return typeof value === "string" && value.trim() ? value : undefined;
297
- }
298
-
299
- function stringArrayValue(value: unknown): string[] {
300
- return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && !!item.trim()) : [];
301
- }
302
-
303
- function sourceLabel(value: unknown): string | undefined {
304
- if (!value || typeof value !== "object") return undefined;
305
- const source = stringValue((value as { source?: unknown }).source);
306
- const path = stringValue((value as { path?: unknown }).path);
307
- if (source && path) return `${source}: ${path}`;
308
- return source ?? path;
309
- }
310
-
311
- function comparePolicyResource(a: WebEditorPolicyResource, b: WebEditorPolicyResource): number {
312
- return a.name.localeCompare(b.name);
313
- }
314
-
315
- function hasPolicyResourceName(resource: WebEditorPolicyResource): boolean {
316
- return !!resource.name.trim();
317
- }
318
-
319
- function sameStringSet(a: string[], b: string[]): boolean {
320
- if (a.length !== b.length) return false;
321
- const bSet = new Set(b);
322
- return a.every((value) => bSet.has(value));
323
- }
324
-
325
- function sharedWebEditorForCwd(cwd: string): SharedWebEditorState {
326
- sharedWebEditors.byCwd[cwd] ??= {};
327
- return sharedWebEditors.byCwd[cwd];
328
- }
329
-
330
- function syncWebEditorFromShared(cwd: string): void {
331
- const shared = sharedWebEditorForCwd(cwd);
332
- webEditor = shared.server;
333
- webEditorCwd = shared.cwd;
334
- webEditorPreferredPort = shared.preferredPort;
335
- }
336
-
337
- function rememberWebEditor(server: WebEditorServer, cwd: string, preferredPort: number | undefined): void {
338
- const shared = sharedWebEditorForCwd(cwd);
339
- webEditor = server;
340
- webEditorCwd = cwd;
341
- webEditorPreferredPort = preferredPort;
342
- shared.server = server;
343
- shared.cwd = cwd;
344
- shared.preferredPort = preferredPort;
345
- }
346
-
347
- function clearWebEditor(server: WebEditorServer): void {
348
- if (webEditor === server) {
349
- webEditor = undefined;
350
- webEditorCwd = undefined;
351
- webEditorPreferredPort = undefined;
352
- }
353
- for (const [cwd, shared] of Object.entries(sharedWebEditors.byCwd)) {
354
- if (shared?.server === server) delete sharedWebEditors.byCwd[cwd];
355
- }
356
- }
357
-
358
- function refreshWebEditorHost(ctx: ExtensionContext): void {
359
- syncWebEditorFromShared(ctx.cwd);
360
- if (!webEditor) return;
361
- const commandCtx = ctx as ExtensionCommandContext;
362
- webEditor.updateHost(createWebEditorHost(commandCtx, webHostRuntime(commandCtx)));
363
- rememberWebEditor(webEditor, ctx.cwd, webEditorPreferredPort);
364
- ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
365
- }
366
-
367
- async function openWebEditor(ctx: ExtensionCommandContext, mode: "open" | "restart" = "open"): Promise<void> {
368
- syncWebEditorFromShared(ctx.cwd);
369
- const settings = loadWebEditorSettings(ctx);
370
- for (const warning of settings.warnings) ctx.ui.notify(warning, "warning");
371
-
372
- if (webEditor && (mode === "restart" || webEditorPreferredPort !== settings.preferredPort)) {
373
- const server = webEditor;
374
- await server.close();
375
- clearWebEditor(server);
376
- ctx.ui.setStatus("pi-forge-editor", undefined);
377
- }
378
-
379
- if (!webEditor) {
380
- try {
381
- webEditor = await startWebEditorServer(createWebEditorHost(ctx, webHostRuntime(ctx)), { port: settings.preferredPort });
382
- } catch (error) {
383
- if (settings.preferredPort !== undefined) {
384
- const detail = error instanceof Error ? error.message : String(error);
385
- ctx.ui.notify(`pi-forge: preferred editor port 127.0.0.1:${settings.preferredPort} was unavailable (${detail}); using an available port instead.`, "warning");
386
- try {
387
- webEditor = await startWebEditorServer(createWebEditorHost(ctx, webHostRuntime(ctx)));
388
- } catch (fallbackError) {
389
- const fallbackDetail = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
390
- ctx.ui.setStatus("pi-forge-editor", undefined);
391
- ctx.ui.notify(`pi-forge: failed to start stack editor on an available localhost port: ${fallbackDetail}.`, "error");
392
- return;
393
- }
394
- } else {
395
- const detail = error instanceof Error ? error.message : String(error);
396
- ctx.ui.setStatus("pi-forge-editor", undefined);
397
- ctx.ui.notify(`pi-forge: failed to start stack editor on an available localhost port: ${detail}.`, "error");
398
- return;
399
- }
400
- }
401
- rememberWebEditor(webEditor, ctx.cwd, settings.preferredPort);
402
- ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
403
- ctx.ui.notify(`pi-forge: stack editor running at ${webEditor.url}`, "info");
404
- } else {
405
- webEditor.updateHost(createWebEditorHost(ctx, webHostRuntime(ctx)));
406
- rememberWebEditor(webEditor, ctx.cwd, settings.preferredPort);
407
- ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
408
- ctx.ui.notify(`pi-forge: stack editor already running at ${webEditor.url}`, "info");
409
- }
410
-
411
- await showText(ctx, "pi-forge stack editor", `Open the local stack editor:\n\n${webEditor.url}\n\nServer bound to 127.0.0.1:${webEditor.port}\nOptional config: ${settings.configPath}\nProject: ${webEditorCwd}`);
412
- }
413
-
414
- async function stopWebEditor(ctx: ExtensionCommandContext): Promise<void> {
415
- syncWebEditorFromShared(ctx.cwd);
416
- if (!webEditor) {
417
- ctx.ui.notify("pi-forge: stack editor is not running.", "info");
418
- return;
419
- }
420
- const server = webEditor;
421
- await server.close();
422
- clearWebEditor(server);
423
- ctx.ui.setStatus("pi-forge-editor", undefined);
424
- ctx.ui.notify("pi-forge: stack editor stopped.", "info");
425
- }
426
-
427
- registerLifecycleHandlers(pi, state, {
428
- reloadStacks,
429
- refreshWebEditorHost,
430
- notifyActivePreset,
431
- syncActiveToolPolicy,
432
- activeId,
433
- persistActiveSelection,
434
- recordCompileDiagnostics,
435
- });
436
- registerPayloadRequestHandler(pi, state, () => state.active);
437
- registerPayloadCommands(pi, state);
438
- registerPresetCommand(pi, state, {
439
- selectedActiveId,
440
- setActive,
441
- reloadStacks,
442
- openWebEditor,
443
- stopWebEditor,
444
- });
445
- }