@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.
- package/README.md +87 -2
- package/README.zh-CN.md +72 -2
- package/dist/compiler.d.ts +10 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +490 -0
- package/dist/compiler.js.map +1 -0
- package/dist/extension-registry.d.ts +25 -0
- package/dist/extension-registry.d.ts.map +1 -0
- package/dist/extension-registry.js +6 -0
- package/dist/extension-registry.js.map +1 -0
- package/dist/forge-extensions.d.ts +28 -0
- package/dist/forge-extensions.d.ts.map +1 -0
- package/dist/forge-extensions.js +182 -0
- package/dist/forge-extensions.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +376 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +14 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +139 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/loader.d.ts +8 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +345 -0
- package/dist/loader.js.map +1 -0
- package/dist/macro-engine.d.ts +23 -0
- package/dist/macro-engine.d.ts.map +1 -0
- package/dist/macro-engine.js +262 -0
- package/dist/macro-engine.js.map +1 -0
- package/dist/payload-capture.d.ts +15 -0
- package/dist/payload-capture.d.ts.map +1 -0
- package/dist/payload-capture.js +86 -0
- package/dist/payload-capture.js.map +1 -0
- package/dist/payload-command.d.ts +10 -0
- package/dist/payload-command.d.ts.map +1 -0
- package/dist/payload-command.js +112 -0
- package/dist/payload-command.js.map +1 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +38 -0
- package/dist/policy.js.map +1 -0
- package/dist/preset-command.d.ts +14 -0
- package/dist/preset-command.d.ts.map +1 -0
- package/dist/preset-command.js +232 -0
- package/dist/preset-command.js.map +1 -0
- package/dist/preview.d.ts +12 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +179 -0
- package/dist/preview.js.map +1 -0
- package/dist/regex.d.ts +7 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +432 -0
- package/dist/regex.js.map +1 -0
- package/dist/render-helpers.d.ts +44 -0
- package/dist/render-helpers.d.ts.map +1 -0
- package/dist/render-helpers.js +128 -0
- package/dist/render-helpers.js.map +1 -0
- package/dist/runtime-state.d.ts +26 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +15 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/sillytavern-importer/items.d.ts +3 -0
- package/dist/sillytavern-importer/items.d.ts.map +1 -0
- package/dist/sillytavern-importer/items.js +88 -0
- package/dist/sillytavern-importer/items.js.map +1 -0
- package/dist/sillytavern-importer/macros.d.ts +15 -0
- package/dist/sillytavern-importer/macros.d.ts.map +1 -0
- package/dist/sillytavern-importer/macros.js +141 -0
- package/dist/sillytavern-importer/macros.js.map +1 -0
- package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
- package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
- package/dist/sillytavern-importer/prompt-order.js +38 -0
- package/dist/sillytavern-importer/prompt-order.js.map +1 -0
- package/dist/sillytavern-importer/regex.d.ts +3 -0
- package/dist/sillytavern-importer/regex.d.ts.map +1 -0
- package/dist/sillytavern-importer/regex.js +275 -0
- package/dist/sillytavern-importer/regex.js.map +1 -0
- package/dist/sillytavern-importer/report.d.ts +21 -0
- package/dist/sillytavern-importer/report.d.ts.map +1 -0
- package/dist/sillytavern-importer/report.js +166 -0
- package/dist/sillytavern-importer/report.js.map +1 -0
- package/dist/sillytavern-importer/types.d.ts +106 -0
- package/dist/sillytavern-importer/types.d.ts.map +1 -0
- package/dist/sillytavern-importer/types.js +2 -0
- package/dist/sillytavern-importer/types.js.map +1 -0
- package/dist/sillytavern-importer.d.ts +5 -0
- package/dist/sillytavern-importer.d.ts.map +1 -0
- package/dist/sillytavern-importer.js +117 -0
- package/dist/sillytavern-importer.js.map +1 -0
- package/dist/slot-renderers.d.ts +25 -0
- package/dist/slot-renderers.d.ts.map +1 -0
- package/dist/slot-renderers.js +334 -0
- package/dist/slot-renderers.js.map +1 -0
- package/dist/stack-migration.d.ts +29 -0
- package/dist/stack-migration.d.ts.map +1 -0
- package/dist/stack-migration.js +126 -0
- package/dist/stack-migration.js.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +36 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web-editor/index.d.ts +3 -0
- package/dist/web-editor/index.d.ts.map +1 -0
- package/dist/web-editor/index.js +2 -0
- package/dist/web-editor/index.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -0
- package/dist/web-editor/page.js +3331 -0
- package/dist/web-editor/page.js.map +1 -0
- package/dist/web-editor/server.d.ts +4 -0
- package/dist/web-editor/server.d.ts.map +1 -0
- package/dist/web-editor/server.js +263 -0
- package/dist/web-editor/server.js.map +1 -0
- package/dist/web-editor/types.d.ts +125 -0
- package/dist/web-editor/types.d.ts.map +1 -0
- package/dist/web-editor/types.js +2 -0
- package/dist/web-editor/types.js.map +1 -0
- package/dist/web-host.d.ts +29 -0
- package/dist/web-host.d.ts.map +1 -0
- package/dist/web-host.js +174 -0
- package/dist/web-host.js.map +1 -0
- package/examples/custom-system-status-extension/README.md +85 -0
- package/examples/custom-system-status-extension/index.ts +136 -0
- package/examples/custom-system-status-extension/prompt-stack.json +39 -0
- package/examples/image-reader-prompt-stack.json +124 -0
- package/package.json +28 -2
- package/src/compiler.ts +2 -472
- package/src/extension-registry.ts +33 -0
- package/src/forge-extensions.ts +223 -0
- package/src/index.ts +208 -653
- package/src/lifecycle.ts +171 -0
- package/src/macro-engine.ts +358 -0
- package/src/payload-command.ts +138 -0
- package/src/preset-command.ts +280 -0
- package/src/preview.ts +226 -0
- package/src/render-helpers.ts +169 -0
- package/src/runtime-state.ts +40 -0
- package/src/sillytavern-importer/items.ts +98 -0
- package/src/sillytavern-importer/macros.ts +159 -0
- package/src/sillytavern-importer/prompt-order.ts +54 -0
- package/src/sillytavern-importer/regex.ts +270 -0
- package/src/sillytavern-importer/report.ts +202 -0
- package/src/sillytavern-importer/types.ts +120 -0
- package/src/sillytavern-importer.ts +55 -865
- package/src/slot-renderers.ts +414 -0
- package/src/storage.ts +17 -0
- package/src/types.ts +3 -14
- package/src/web-editor/page.ts +516 -30
- package/src/web-editor/server.ts +9 -4
- package/src/web-editor/types.ts +18 -4
- package/src/web-host.ts +16 -13
package/src/index.ts
CHANGED
|
@@ -1,31 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
compileSystemPrompt,
|
|
7
|
-
createPromptVariableStore,
|
|
8
|
-
agentMessageToPreviewText,
|
|
9
|
-
getLatestUserMessage,
|
|
10
|
-
markSessionVariablesClean,
|
|
11
|
-
resetTurnVariables,
|
|
12
|
-
} from "./compiler.ts";
|
|
13
|
-
import { chooseDefaultStack, isDisabledPromptStackId, loadPromptStacks, promptStackPath, promptStackReadDirs } from "./loader.ts";
|
|
14
|
-
import { createProviderPayloadCapture, estimatePayloadTokens } from "./payload-capture.ts";
|
|
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";
|
|
15
6
|
import { applyResourcePolicy, hasResourcePolicy } from "./policy.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import type {
|
|
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";
|
|
20
11
|
import { createWebEditorHost, loadWebEditorSettings, type WebHostRuntime } from "./web-host.ts";
|
|
21
|
-
import { startWebEditorServer, type
|
|
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";
|
|
22
47
|
|
|
23
|
-
const STATE_ENTRY_TYPE = "pi-forge-prompt-stack-state";
|
|
24
|
-
const VARIABLE_ENTRY_TYPE = "pi-forge-variable-state";
|
|
25
48
|
const WEB_EDITOR_GLOBAL_KEY = "__piForgeWebEditor";
|
|
26
49
|
|
|
27
|
-
type PayloadDisplayTarget = "editor" | "web";
|
|
28
|
-
|
|
29
50
|
interface SharedWebEditorState {
|
|
30
51
|
server?: WebEditorServer;
|
|
31
52
|
cwd?: string;
|
|
@@ -48,86 +69,82 @@ function getSharedWebEditorRegistry(): SharedWebEditorRegistry {
|
|
|
48
69
|
|
|
49
70
|
export default function piForge(pi: ExtensionAPI) {
|
|
50
71
|
const sharedWebEditors = getSharedWebEditorRegistry();
|
|
51
|
-
|
|
52
|
-
let active: LoadedPromptStack | undefined;
|
|
53
|
-
let currentSystemPromptOptions: BuildSystemPromptOptions | undefined;
|
|
54
|
-
let currentLatestUserMessage: string | undefined;
|
|
55
|
-
let currentVariableStore: PromptVariableStore | undefined;
|
|
56
|
-
let contextRewritePending = false;
|
|
57
|
-
let sessionVariables: Record<string, PromptVariableValue> = {};
|
|
58
|
-
let lastPersistedActiveId: string | undefined;
|
|
59
|
-
let interceptNextProviderPayload = false;
|
|
60
|
-
let interceptPayloadSavePath: string | undefined;
|
|
61
|
-
let interceptPayloadDisplayTarget: PayloadDisplayTarget = "editor";
|
|
62
|
-
let payloadCaptureArmedAt: string | undefined;
|
|
63
|
-
let latestProviderPayloadCapture: WebEditorPayloadCapture | undefined;
|
|
64
|
-
let latestCompileDiagnostics: PromptStackDiagnostic[] = [];
|
|
72
|
+
const state = createRuntimeState();
|
|
65
73
|
let webEditor: WebEditorServer | undefined;
|
|
66
74
|
let webEditorCwd: string | undefined;
|
|
67
75
|
let webEditorPreferredPort: number | undefined;
|
|
68
76
|
let toolPolicyBaseline: string[] | undefined;
|
|
77
|
+
const forgeExtensionState = createForgeExtensionState();
|
|
69
78
|
|
|
70
79
|
function activeId(): string | undefined {
|
|
71
|
-
return active?.stack.id;
|
|
80
|
+
return state.active?.stack.id;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
function selectedActiveId(): string | undefined {
|
|
75
|
-
|
|
76
|
-
return isDisabledPromptStackId(lastPersistedActiveId) ? "none" : undefined;
|
|
84
|
+
return selectedActiveIdForState(state);
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
function persistActiveSelection(id: string): void {
|
|
80
|
-
if (id === lastPersistedActiveId) return;
|
|
88
|
+
if (id === state.lastPersistedActiveId) return;
|
|
81
89
|
pi.appendEntry(STATE_ENTRY_TYPE, { activeStackId: id });
|
|
82
|
-
lastPersistedActiveId = id;
|
|
90
|
+
state.lastPersistedActiveId = id;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function setActive(id: string | undefined, ctx?: ExtensionContext): boolean {
|
|
86
94
|
if (!id || isDisabledPromptStackId(id)) {
|
|
87
|
-
active = undefined;
|
|
95
|
+
state.active = undefined;
|
|
88
96
|
if (id) persistActiveSelection("none");
|
|
89
97
|
if (ctx) updateStatus(ctx);
|
|
90
98
|
syncActiveToolPolicy(ctx);
|
|
91
99
|
return true;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
const found = stacks.find((candidate) => candidate.stack.id === id);
|
|
102
|
+
const found = state.stacks.find((candidate) => candidate.stack.id === id);
|
|
95
103
|
if (!found) return false;
|
|
96
|
-
active = found;
|
|
104
|
+
state.active = found;
|
|
97
105
|
persistActiveSelection(found.stack.id);
|
|
98
106
|
if (ctx) updateStatus(ctx);
|
|
99
107
|
syncActiveToolPolicy(ctx);
|
|
100
108
|
return true;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
function reloadStacks(ctx: ExtensionContext, preferredId?: string): void {
|
|
111
|
+
async function reloadStacks(ctx: ExtensionContext, preferredId?: string): Promise<void> {
|
|
104
112
|
if (!ctx.isProjectTrusted()) {
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
const unloadDiagnostics = unloadForgeExtensions(forgeExtensionState);
|
|
114
|
+
state.forgeExtensionDiagnostics = unloadDiagnostics;
|
|
115
|
+
state.forgeExtensionPaths = [];
|
|
116
|
+
state.stacks = [];
|
|
117
|
+
state.active = undefined;
|
|
107
118
|
syncActiveToolPolicy(ctx);
|
|
108
119
|
ctx.ui.notify("pi-forge: project is not trusted; prompt stacks are disabled.", "warning");
|
|
109
120
|
updateStatus(ctx);
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
115
132
|
updateStatus(ctx);
|
|
116
133
|
syncActiveToolPolicy(ctx);
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
function updateStatus(ctx: ExtensionContext): void {
|
|
120
|
-
if (active) {
|
|
121
|
-
ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + active.stack.id));
|
|
137
|
+
if (state.active) {
|
|
138
|
+
ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + state.active.stack.id));
|
|
122
139
|
} else {
|
|
123
140
|
ctx.ui.setStatus("pi-forge", undefined);
|
|
124
|
-
latestCompileDiagnostics = [];
|
|
141
|
+
state.latestCompileDiagnostics = [];
|
|
125
142
|
ctx.ui.setStatus("pi-forge-diagnostics", undefined);
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
function syncActiveToolPolicy(ctx?: ExtensionContext): void {
|
|
130
|
-
const policy = active?.stack.tools;
|
|
147
|
+
const policy = state.active?.stack.tools;
|
|
131
148
|
if (!hasResourcePolicy(policy)) {
|
|
132
149
|
restoreToolPolicy(ctx);
|
|
133
150
|
return;
|
|
@@ -158,15 +175,15 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
158
175
|
}
|
|
159
176
|
|
|
160
177
|
function notifyActivePreset(ctx: ExtensionContext, detail: string): void {
|
|
161
|
-
if (!active) return;
|
|
162
|
-
const errorCount = active.diagnostics.filter((d) => d.level === "error").length;
|
|
163
|
-
const warningCount = active.diagnostics.filter((d) => d.level === "warning").length;
|
|
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;
|
|
164
181
|
const suffix = errorCount || warningCount ? " (" + errorCount + " errors, " + warningCount + " warnings)" : "";
|
|
165
|
-
ctx.ui.notify("pi-forge: active preset " + active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
|
|
182
|
+
ctx.ui.notify("pi-forge: active preset " + state.active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
function recordCompileDiagnostics(ctx: ExtensionContext, diagnostics: PromptStackDiagnostic[]): void {
|
|
169
|
-
latestCompileDiagnostics = diagnostics;
|
|
186
|
+
state.latestCompileDiagnostics = diagnostics;
|
|
170
187
|
const errors = diagnostics.filter((d) => d.level === "error").length;
|
|
171
188
|
const warnings = diagnostics.filter((d) => d.level === "warning").length;
|
|
172
189
|
if (errors || warnings) {
|
|
@@ -178,25 +195,133 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
178
195
|
|
|
179
196
|
function webHostRuntime(ctx: ExtensionCommandContext): WebHostRuntime {
|
|
180
197
|
return {
|
|
181
|
-
getStacks: () => stacks,
|
|
182
|
-
getActive: () => active,
|
|
198
|
+
getStacks: () => state.stacks,
|
|
199
|
+
getActive: () => state.active,
|
|
183
200
|
getActiveId: activeId,
|
|
184
201
|
getSelectedActiveId: selectedActiveId,
|
|
185
202
|
setActive: (id) => setActive(id, ctx),
|
|
186
203
|
reloadStacks: (preferredId) => reloadStacks(ctx, preferredId),
|
|
187
|
-
buildPreview: (target) => buildPreview(ctx, target),
|
|
188
|
-
|
|
204
|
+
buildPreview: (target) => buildPreview(ctx, target, state.sessionVariables, previewOptionsForStack(ctx, target.stack)),
|
|
205
|
+
getPolicyResources: () => getPolicyResources(ctx),
|
|
206
|
+
getPayload: () => ({ ok: true, ...webPayloadSnapshot(state) }),
|
|
189
207
|
armPayload: (savePath) => {
|
|
190
|
-
armPayloadIntercept(ctx, savePath, "web");
|
|
191
|
-
return { ok: true, ...webPayloadSnapshot() };
|
|
208
|
+
armPayloadIntercept(state, ctx, savePath, "web");
|
|
209
|
+
return { ok: true, ...webPayloadSnapshot(state) };
|
|
192
210
|
},
|
|
193
211
|
clearPayload: () => {
|
|
194
|
-
clearPayloadCapture(ctx);
|
|
195
|
-
return { ok: true, ...webPayloadSnapshot() };
|
|
212
|
+
clearPayloadCapture(state, ctx);
|
|
213
|
+
return { ok: true, ...webPayloadSnapshot(state) };
|
|
196
214
|
},
|
|
197
215
|
};
|
|
198
216
|
}
|
|
199
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
|
+
|
|
200
325
|
function sharedWebEditorForCwd(cwd: string): SharedWebEditorState {
|
|
201
326
|
sharedWebEditors.byCwd[cwd] ??= {};
|
|
202
327
|
return sharedWebEditors.byCwd[cwd];
|
|
@@ -299,592 +424,22 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
299
424
|
ctx.ui.notify("pi-forge: stack editor stopped.", "info");
|
|
300
425
|
}
|
|
301
426
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function getRestoredActiveId(ctx: ExtensionContext): string | undefined {
|
|
313
|
-
const entries = getCurrentBranchEntries(ctx);
|
|
314
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
315
|
-
const entry = entries[i] as { type?: string; customType?: string; data?: { activeStackId?: unknown } };
|
|
316
|
-
if (entry.type === "custom" && entry.customType === STATE_ENTRY_TYPE) {
|
|
317
|
-
return typeof entry.data?.activeStackId === "string" ? entry.data.activeStackId : undefined;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function getRestoredVariables(ctx: ExtensionContext): Record<string, PromptVariableValue> {
|
|
324
|
-
const entries = getCurrentBranchEntries(ctx);
|
|
325
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
326
|
-
const entry = entries[i] as { type?: string; customType?: string; data?: { variables?: unknown } };
|
|
327
|
-
if (entry.type !== "custom" || entry.customType !== VARIABLE_ENTRY_TYPE) continue;
|
|
328
|
-
if (!entry.data || typeof entry.data.variables !== "object" || Array.isArray(entry.data.variables)) return {};
|
|
329
|
-
return normalizeVariableRecord(entry.data.variables as Record<string, unknown>);
|
|
330
|
-
}
|
|
331
|
-
return {};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function persistVariablesIfDirty(store: PromptVariableStore | undefined): void {
|
|
335
|
-
if (!store?.sessionDirty) return;
|
|
336
|
-
sessionVariables = { ...store.session };
|
|
337
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: sessionVariables });
|
|
338
|
-
markSessionVariablesClean(store);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function normalizeVariableRecord(value: Record<string, unknown>): Record<string, PromptVariableValue> {
|
|
342
|
-
const result: Record<string, PromptVariableValue> = {};
|
|
343
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
344
|
-
if (isPromptVariableValue(raw)) result[key] = raw;
|
|
345
|
-
}
|
|
346
|
-
return result;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function isPromptVariableValue(value: unknown): value is PromptVariableValue {
|
|
350
|
-
if (value === null) return true;
|
|
351
|
-
const type = typeof value;
|
|
352
|
-
if (type === "string" || type === "boolean") return true;
|
|
353
|
-
if (type === "number") return Number.isFinite(value);
|
|
354
|
-
if (Array.isArray(value)) return value.every(isPromptVariableValue);
|
|
355
|
-
if (!value || typeof value !== "object") return false;
|
|
356
|
-
return Object.values(value as Record<string, unknown>).every(isPromptVariableValue);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function restoreBranchScopedRuntime(ctx: ExtensionContext): void {
|
|
360
|
-
sessionVariables = getRestoredVariables(ctx);
|
|
361
|
-
currentVariableStore = undefined;
|
|
362
|
-
const restoredActiveId = getRestoredActiveId(ctx);
|
|
363
|
-
lastPersistedActiveId = restoredActiveId;
|
|
364
|
-
reloadStacks(ctx, restoredActiveId);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
pi.on("session_start", async (event, ctx) => {
|
|
368
|
-
restoreBranchScopedRuntime(ctx);
|
|
369
|
-
refreshWebEditorHost(ctx);
|
|
370
|
-
notifyActivePreset(ctx, "after session " + event.reason);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
374
|
-
restoreBranchScopedRuntime(ctx);
|
|
375
|
-
refreshWebEditorHost(ctx);
|
|
376
|
-
notifyActivePreset(ctx, "after tree navigation");
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
pi.on("session_compact", async (_event, ctx) => {
|
|
380
|
-
restoreBranchScopedRuntime(ctx);
|
|
381
|
-
refreshWebEditorHost(ctx);
|
|
382
|
-
notifyActivePreset(ctx, "after compaction");
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
pi.on("turn_start", async () => {
|
|
386
|
-
syncActiveToolPolicy();
|
|
387
|
-
const id = activeId();
|
|
388
|
-
if (id) persistActiveSelection(id);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
392
|
-
currentSystemPromptOptions = event.systemPromptOptions;
|
|
393
|
-
currentLatestUserMessage = event.prompt;
|
|
394
|
-
currentVariableStore = createPromptVariableStore(sessionVariables);
|
|
395
|
-
resetTurnVariables(currentVariableStore);
|
|
396
|
-
contextRewritePending = true;
|
|
397
|
-
|
|
398
|
-
if (!active) return;
|
|
399
|
-
|
|
400
|
-
const result = compileSystemPrompt(
|
|
401
|
-
active.stack,
|
|
402
|
-
{ options: event.systemPromptOptions, ctx, latestUserMessage: event.prompt, now: new Date(), variables: currentVariableStore },
|
|
403
|
-
event.systemPrompt,
|
|
404
|
-
);
|
|
405
|
-
recordCompileDiagnostics(ctx, result.diagnostics);
|
|
406
|
-
persistVariablesIfDirty(currentVariableStore);
|
|
407
|
-
|
|
408
|
-
return { systemPrompt: result.systemPrompt };
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
pi.on("context", async (event, ctx) => {
|
|
412
|
-
if (!active || !currentSystemPromptOptions || !contextRewritePending) return;
|
|
413
|
-
|
|
414
|
-
// Rewrite the message layout only for the first provider request of a user-submitted prompt.
|
|
415
|
-
// Tool-result follow-up turns must receive Pi's natural context; otherwise post-history
|
|
416
|
-
// prompt blocks such as COT / {{lastUserMessage}} are re-appended after every tool call
|
|
417
|
-
// and the model restarts its planning instead of continuing from the tool result.
|
|
418
|
-
contextRewritePending = false;
|
|
419
|
-
|
|
420
|
-
if (!currentVariableStore) currentVariableStore = createPromptVariableStore(sessionVariables);
|
|
421
|
-
const latestUserMessage = getLatestUserMessage(event.messages) ?? currentLatestUserMessage;
|
|
422
|
-
const result = compileMessages(
|
|
423
|
-
active.stack,
|
|
424
|
-
{ options: currentSystemPromptOptions, ctx, latestUserMessage, now: new Date(), variables: currentVariableStore },
|
|
425
|
-
event.messages,
|
|
426
|
-
);
|
|
427
|
-
recordCompileDiagnostics(ctx, [...latestCompileDiagnostics, ...result.diagnostics]);
|
|
428
|
-
persistVariablesIfDirty(currentVariableStore);
|
|
429
|
-
return { messages: result.messages };
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
pi.on("message_end", async (event, ctx) => {
|
|
433
|
-
if (!active) return;
|
|
434
|
-
const diagnostics: PromptStackDiagnostic[] = [];
|
|
435
|
-
const message = applyFinalizeRegexRulesToMessage(active.stack, event.message, diagnostics);
|
|
436
|
-
if (diagnostics.length > 0) recordCompileDiagnostics(ctx, [...latestCompileDiagnostics, ...diagnostics]);
|
|
437
|
-
if (!message) return;
|
|
438
|
-
return { message };
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
pi.on("agent_end", async () => {
|
|
442
|
-
persistVariablesIfDirty(currentVariableStore);
|
|
443
|
-
currentSystemPromptOptions = undefined;
|
|
444
|
-
currentLatestUserMessage = undefined;
|
|
445
|
-
currentVariableStore = undefined;
|
|
446
|
-
contextRewritePending = false;
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
pi.on("before_provider_request", async (event, ctx) => {
|
|
450
|
-
if (!interceptNextProviderPayload) return;
|
|
451
|
-
const savePath = interceptPayloadSavePath;
|
|
452
|
-
const displayTarget = interceptPayloadDisplayTarget;
|
|
453
|
-
interceptNextProviderPayload = false;
|
|
454
|
-
interceptPayloadSavePath = undefined;
|
|
455
|
-
interceptPayloadDisplayTarget = "editor";
|
|
456
|
-
payloadCaptureArmedAt = undefined;
|
|
457
|
-
ctx.ui.setStatus("pi-forge-intercept", undefined);
|
|
458
|
-
|
|
459
|
-
const capture = captureProviderPayload(event.payload, savePath);
|
|
460
|
-
if (savePath) {
|
|
461
|
-
if (!ctx.isProjectTrusted()) {
|
|
462
|
-
ctx.ui.notify("pi-forge: project is not trusted; refusing to save provider payload.", "warning");
|
|
463
|
-
} else {
|
|
464
|
-
const resolvedPath = savePath.startsWith("/") ? savePath : join(ctx.cwd, savePath);
|
|
465
|
-
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
466
|
-
writeFileSync(resolvedPath, capture.text, "utf8");
|
|
467
|
-
ctx.ui.notify(`pi-forge: provider payload saved to ${resolvedPath} (${capture.chars} chars, ~${capture.approxTokens} tokens)`, "info");
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (displayTarget === "web") {
|
|
472
|
-
ctx.ui.notify(`pi-forge: provider payload captured for web editor (${capture.chars} chars, ~${capture.approxTokens} tokens).`, "info");
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (ctx.hasUI) {
|
|
477
|
-
await ctx.ui.editor(`pi-forge: provider payload (${capture.chars} chars, ~${capture.approxTokens} tokens)`, capture.text);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
console.log(capture.text);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
function armPayloadIntercept(ctx: ExtensionCommandContext, savePath?: string, displayTarget: PayloadDisplayTarget = "editor"): void {
|
|
485
|
-
interceptNextProviderPayload = true;
|
|
486
|
-
interceptPayloadSavePath = savePath;
|
|
487
|
-
interceptPayloadDisplayTarget = displayTarget;
|
|
488
|
-
payloadCaptureArmedAt = new Date().toISOString();
|
|
489
|
-
latestProviderPayloadCapture = undefined;
|
|
490
|
-
ctx.ui.setStatus("pi-forge-intercept", ctx.ui.theme.fg("warning", savePath ? "payload:armed+save" : "payload:armed"));
|
|
491
|
-
if (displayTarget === "web") {
|
|
492
|
-
ctx.ui.notify(savePath ? `pi-forge: next provider payload will be captured in the web editor and saved to ${savePath}.` : "pi-forge: next provider payload will be captured in the web editor.", "info");
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
ctx.ui.notify(savePath ? `pi-forge: next provider payload will be displayed and saved to ${savePath}.` : "pi-forge: next provider payload will be displayed before sending.", "info");
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function clearPayloadCapture(ctx: ExtensionCommandContext): void {
|
|
499
|
-
interceptNextProviderPayload = false;
|
|
500
|
-
interceptPayloadSavePath = undefined;
|
|
501
|
-
interceptPayloadDisplayTarget = "editor";
|
|
502
|
-
payloadCaptureArmedAt = undefined;
|
|
503
|
-
latestProviderPayloadCapture = undefined;
|
|
504
|
-
ctx.ui.setStatus("pi-forge-intercept", undefined);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
pi.registerCommand("intercept", {
|
|
508
|
-
description: "Display the next provider payload before it is sent",
|
|
509
|
-
handler: async (_args, ctx) => {
|
|
510
|
-
armPayloadIntercept(ctx);
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
pi.registerCommand("payload", {
|
|
515
|
-
description: "Inspect or save provider payloads: /payload next [save=<path>]",
|
|
516
|
-
getArgumentCompletions: (prefix) => {
|
|
517
|
-
const parts = prefix.trimStart().split(/\s+/);
|
|
518
|
-
if (parts.length <= 1 && !prefix.endsWith(" ")) {
|
|
519
|
-
return ["next"].filter((cmd) => cmd.startsWith(parts[0] ?? "")).map((cmd) => ({ value: cmd, label: cmd }));
|
|
520
|
-
}
|
|
521
|
-
if (parts[0] === "next" && parts.length <= 2) {
|
|
522
|
-
const suggestion = "save=.pi/forge/payloads/last.json";
|
|
523
|
-
return suggestion.startsWith(parts[1] ?? "") ? [{ value: `next ${suggestion}`, label: suggestion }] : null;
|
|
524
|
-
}
|
|
525
|
-
return null;
|
|
526
|
-
},
|
|
527
|
-
handler: async (args, ctx) => {
|
|
528
|
-
const [command = "next", ...rest] = args.trim() ? args.trim().split(/\s+/) : ["next"];
|
|
529
|
-
if (command !== "next") {
|
|
530
|
-
ctx.ui.notify(`Unknown /payload subcommand: ${command}`, "warning");
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
const saveArg = rest.find((arg) => arg.startsWith("save="));
|
|
534
|
-
const savePath = saveArg?.slice("save=".length).trim() || undefined;
|
|
535
|
-
armPayloadIntercept(ctx, savePath);
|
|
536
|
-
},
|
|
427
|
+
registerLifecycleHandlers(pi, state, {
|
|
428
|
+
reloadStacks,
|
|
429
|
+
refreshWebEditorHost,
|
|
430
|
+
notifyActivePreset,
|
|
431
|
+
syncActiveToolPolicy,
|
|
432
|
+
activeId,
|
|
433
|
+
persistActiveSelection,
|
|
434
|
+
recordCompileDiagnostics,
|
|
537
435
|
});
|
|
538
|
-
|
|
539
|
-
pi
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
const first = parts[0];
|
|
548
|
-
if (["use", "preview", "validate"].includes(first)) {
|
|
549
|
-
const fragment = parts[1] ?? "";
|
|
550
|
-
const ids = ["none", ...stacks.map((loaded) => loaded.stack.id)];
|
|
551
|
-
return ids.filter((id) => id.startsWith(fragment)).map((id) => ({ value: `${first} ${id}`, label: id }));
|
|
552
|
-
}
|
|
553
|
-
if (first === "ui" && parts.length <= 2) {
|
|
554
|
-
const fragment = parts[1] ?? "";
|
|
555
|
-
const subs = ["stop", "restart"];
|
|
556
|
-
return subs.filter((s) => s.startsWith(fragment)).map((s) => ({ value: `ui ${s}`, label: s }));
|
|
557
|
-
}
|
|
558
|
-
if (first === "migrate-stacks") {
|
|
559
|
-
const fragment = parts[parts.length - 1] ?? "";
|
|
560
|
-
const flags = ["--dry-run", "--overwrite", "--delete-legacy"];
|
|
561
|
-
return flags.filter((flag) => flag.startsWith(fragment)).map((flag) => ({ value: `${parts.slice(0, -1).join(" ")} ${flag}`.trim(), label: flag }));
|
|
562
|
-
}
|
|
563
|
-
return null;
|
|
564
|
-
},
|
|
565
|
-
handler: async (args, ctx) => {
|
|
566
|
-
await handlePresetCommand(args, ctx);
|
|
567
|
-
},
|
|
436
|
+
registerPayloadRequestHandler(pi, state, () => state.active);
|
|
437
|
+
registerPayloadCommands(pi, state);
|
|
438
|
+
registerPresetCommand(pi, state, {
|
|
439
|
+
selectedActiveId,
|
|
440
|
+
setActive,
|
|
441
|
+
reloadStacks,
|
|
442
|
+
openWebEditor,
|
|
443
|
+
stopWebEditor,
|
|
568
444
|
});
|
|
569
|
-
|
|
570
|
-
async function handlePresetCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
571
|
-
const trimmed = args.trim();
|
|
572
|
-
const [command = "list", ...rest] = trimmed ? trimmed.split(/\s+/) : ["list"];
|
|
573
|
-
|
|
574
|
-
switch (command) {
|
|
575
|
-
case "list":
|
|
576
|
-
case "status":
|
|
577
|
-
await showText(ctx, "pi-forge prompt stacks", renderStackList(ctx));
|
|
578
|
-
return;
|
|
579
|
-
|
|
580
|
-
case "reload":
|
|
581
|
-
reloadStacks(ctx, selectedActiveId());
|
|
582
|
-
ctx.ui.notify(`pi-forge: reloaded ${stacks.length} prompt stack(s).`, "info");
|
|
583
|
-
return;
|
|
584
|
-
|
|
585
|
-
case "ui": {
|
|
586
|
-
const sub = rest[0];
|
|
587
|
-
if (sub === "stop") {
|
|
588
|
-
await stopWebEditor(ctx);
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
await openWebEditor(ctx, sub === "restart" ? "restart" : "open");
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
case "use": {
|
|
596
|
-
const id = rest[0];
|
|
597
|
-
if (!id) {
|
|
598
|
-
ctx.ui.notify("Usage: /preset use <id|none>", "warning");
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (!setActive(id, ctx)) {
|
|
602
|
-
ctx.ui.notify(`Unknown prompt stack: ${id}`, "error");
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
ctx.ui.notify(active ? `pi-forge: active prompt stack ${active.stack.id}` : "pi-forge: prompt stack disabled", "info");
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
case "preview": {
|
|
610
|
-
const target = rest[0] ? stacks.find((loaded) => loaded.stack.id === rest[0]) : active;
|
|
611
|
-
if (!target) {
|
|
612
|
-
ctx.ui.notify(rest[0] ? `Unknown prompt stack: ${rest[0]}` : "No active prompt stack.", "warning");
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
await showText(ctx, `pi-forge preview: ${target.stack.id}`, renderPreview(ctx, target));
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
case "validate": {
|
|
620
|
-
const target = rest[0] ? stacks.find((loaded) => loaded.stack.id === rest[0]) : active;
|
|
621
|
-
if (!target) {
|
|
622
|
-
ctx.ui.notify(rest[0] ? `Unknown prompt stack: ${rest[0]}` : "No active prompt stack.", "warning");
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
await showText(ctx, `pi-forge validation: ${target.stack.id}`, renderDiagnostics(target.diagnostics));
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
case "diagnostics": {
|
|
630
|
-
await showText(ctx, "pi-forge diagnostics", renderCurrentDiagnostics());
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
case "migrate-stacks": {
|
|
635
|
-
const flags = new Set(rest);
|
|
636
|
-
const dryRun = flags.has("--dry-run");
|
|
637
|
-
if (!ctx.isProjectTrusted() && !dryRun) {
|
|
638
|
-
ctx.ui.notify("pi-forge: project is not trusted; refusing to migrate prompt stacks.", "warning");
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
const report = migrateLegacyPromptStacks(ctx.cwd, {
|
|
642
|
-
dryRun,
|
|
643
|
-
overwrite: flags.has("--overwrite"),
|
|
644
|
-
deleteLegacy: flags.has("--delete-legacy"),
|
|
645
|
-
});
|
|
646
|
-
if (!dryRun) reloadStacks(ctx, selectedActiveId());
|
|
647
|
-
const changed = report.copied + report.overwritten;
|
|
648
|
-
const summary = dryRun
|
|
649
|
-
? `pi-forge: migration dry run found ${report.files.length} legacy stack file(s).`
|
|
650
|
-
: `pi-forge: migrated ${changed} legacy stack file(s), skipped ${report.skipped}, errors ${report.errors}.`;
|
|
651
|
-
ctx.ui.notify(summary, report.errors ? "warning" : "info");
|
|
652
|
-
await showText(ctx, "pi-forge prompt-stack migration", renderMigrationReport(report));
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
case "import-silly": {
|
|
657
|
-
if (!ctx.isProjectTrusted()) {
|
|
658
|
-
ctx.ui.notify("pi-forge: project is not trusted; refusing to write imported prompt stacks.", "warning");
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const sourcePath = rest[0];
|
|
663
|
-
if (!sourcePath) {
|
|
664
|
-
ctx.ui.notify("Usage: /preset import-silly <path> [character_id] [--dry-run] [--overwrite]", "warning");
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const charIdToken = rest[1]?.startsWith("--") ? undefined : rest[1];
|
|
669
|
-
const flags = new Set(rest.slice(charIdToken ? 2 : 1));
|
|
670
|
-
const dryRun = flags.has("--dry-run");
|
|
671
|
-
let overwrite = flags.has("--overwrite");
|
|
672
|
-
|
|
673
|
-
const resolvedPath = sourcePath.startsWith("/") ? sourcePath : join(ctx.cwd, sourcePath);
|
|
674
|
-
if (!existsSync(resolvedPath)) {
|
|
675
|
-
ctx.ui.notify(`File not found: ${resolvedPath}`, "error");
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const charId = charIdToken ? Number(charIdToken) : undefined;
|
|
680
|
-
if (charIdToken && (Number.isNaN(charId) || !Number.isFinite(charId))) {
|
|
681
|
-
ctx.ui.notify(`Invalid character_id: ${charIdToken}`, "error");
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const result = importSillyTavernPreset(resolvedPath, charId);
|
|
686
|
-
if ("error" in result) {
|
|
687
|
-
ctx.ui.notify(`pi-forge import error: ${result.error}`, "error");
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const existingStack = stacks.find((candidate) => candidate.stack.id === result.stack.id);
|
|
692
|
-
const stackPath = existingStack?.filePath ?? promptStackPath(ctx.cwd, result.stack.id);
|
|
693
|
-
const stacksDir = dirname(stackPath);
|
|
694
|
-
const reportDir = join(ctx.cwd, ".pi", "forge", "import-reports");
|
|
695
|
-
const reportPath = join(reportDir, `${result.stack.id}.md`);
|
|
696
|
-
|
|
697
|
-
if (dryRun) {
|
|
698
|
-
await showText(ctx, `pi-forge import dry run: ${result.stack.id}`, `Would write stack to: ${stackPath}\nWould write report to: ${reportPath}\n\n## Generated stack JSON\n\n\`\`\`json\n${JSON.stringify(result.stack, null, 2)}\n\`\`\`\n\n${result.report}`);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const existingPaths = [stackPath, reportPath].filter((path) => existsSync(path));
|
|
703
|
-
if (existingPaths.length > 0 && !overwrite) {
|
|
704
|
-
if (!ctx.hasUI) {
|
|
705
|
-
ctx.ui.notify(`pi-forge: import would overwrite existing file(s): ${existingPaths.join(", ")}. Re-run with --overwrite.`, "error");
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
overwrite = await ctx.ui.confirm("Overwrite pi-forge import output?", `These file(s) already exist:\n${existingPaths.join("\n")}\n\nOverwrite them?`);
|
|
709
|
-
if (!overwrite) {
|
|
710
|
-
ctx.ui.notify("pi-forge: import cancelled; existing files were left unchanged.", "info");
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
if (!existsSync(stacksDir)) mkdirSync(stacksDir, { recursive: true });
|
|
716
|
-
writeFileSync(stackPath, JSON.stringify(result.stack, null, 2), "utf8");
|
|
717
|
-
|
|
718
|
-
if (!existsSync(reportDir)) mkdirSync(reportDir, { recursive: true });
|
|
719
|
-
writeFileSync(reportPath, result.report, "utf8");
|
|
720
|
-
|
|
721
|
-
// Reload stacks to pick up the new one
|
|
722
|
-
reloadStacks(ctx, selectedActiveId());
|
|
723
|
-
|
|
724
|
-
ctx.ui.notify(`pi-forge: imported ${result.stack.id} (${result.stack.items.length} items)`, "info");
|
|
725
|
-
await showText(ctx, `pi-forge import report: ${result.stack.id}`, `Stack written to: ${stackPath}\nReport written to: ${reportPath}\n\n${result.report}`);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
default:
|
|
730
|
-
ctx.ui.notify(`Unknown /preset subcommand: ${command}`, "warning");
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function renderStackList(ctx: ExtensionCommandContext): string {
|
|
736
|
-
const lines = [
|
|
737
|
-
"Prompt stack directories:",
|
|
738
|
-
...promptStackReadDirs(ctx.cwd).map((dir, index) => ` ${index === 0 ? "primary" : "legacy"}: ${dir}`),
|
|
739
|
-
`Active stack: ${active?.stack.id ?? "(none)"}`,
|
|
740
|
-
"",
|
|
741
|
-
];
|
|
742
|
-
|
|
743
|
-
if (stacks.length === 0) {
|
|
744
|
-
lines.push("No prompt stacks found.", "Create .pi/forge/prompt-stacks/default.json to auto-activate a stack.");
|
|
745
|
-
return lines.join("\n");
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
for (const loaded of stacks) {
|
|
749
|
-
const marker = loaded === active ? "*" : " ";
|
|
750
|
-
const errors = loaded.diagnostics.filter((d) => d.level === "error").length;
|
|
751
|
-
const warnings = loaded.diagnostics.filter((d) => d.level === "warning").length;
|
|
752
|
-
const suffix = errors || warnings ? ` (${errors} errors, ${warnings} warnings)` : "";
|
|
753
|
-
lines.push(`${marker} ${loaded.stack.id}${loaded.stack.name ? ` — ${loaded.stack.name}` : ""}${suffix}`);
|
|
754
|
-
lines.push(` ${loaded.filePath}`);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
lines.push("", "Commands:", " /preset use <id|none>", " /preset preview [id]", " /preset validate [id]", " /preset diagnostics", " /preset reload", " /preset migrate-stacks [--dry-run] [--overwrite] [--delete-legacy]", " /preset ui [stop|restart]");
|
|
758
|
-
return lines.join("\n");
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function renderCurrentDiagnostics(): string {
|
|
762
|
-
const lines = ["# pi-forge diagnostics", ""];
|
|
763
|
-
lines.push("## Active stack load/validation diagnostics", "");
|
|
764
|
-
lines.push(active ? renderDiagnostics(active.diagnostics) : "No active prompt stack.");
|
|
765
|
-
lines.push("", "## Latest runtime compile diagnostics", "");
|
|
766
|
-
lines.push(renderDiagnostics(latestCompileDiagnostics));
|
|
767
|
-
return lines.join("\n");
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function webPayloadSnapshot(): WebEditorPayloadSnapshot {
|
|
771
|
-
if (interceptNextProviderPayload) {
|
|
772
|
-
return {
|
|
773
|
-
status: "armed",
|
|
774
|
-
armedAt: payloadCaptureArmedAt,
|
|
775
|
-
savePath: interceptPayloadSavePath,
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
if (latestProviderPayloadCapture) {
|
|
779
|
-
return {
|
|
780
|
-
status: "captured",
|
|
781
|
-
capture: latestProviderPayloadCapture,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
return { status: "idle" };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function renderPreview(ctx: ExtensionCommandContext, target: LoadedPromptStack): string {
|
|
788
|
-
return buildPreview(ctx, target).text;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function buildPreview(ctx: ExtensionCommandContext, target: LoadedPromptStack): { text: string; preview: WebEditorPreview; diagnostics: PromptStackDiagnostic[] } {
|
|
792
|
-
const options = ctx.getSystemPromptOptions();
|
|
793
|
-
const sessionContext = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
|
|
794
|
-
const latestUserMessage = getLatestUserMessage(sessionContext.messages);
|
|
795
|
-
const previewVariables = createPromptVariableStore(sessionVariables);
|
|
796
|
-
const runtime = { options, ctx, latestUserMessage, now: new Date(), variables: previewVariables };
|
|
797
|
-
const system = compileSystemPrompt(target.stack, runtime, ctx.getSystemPrompt());
|
|
798
|
-
const messages = compileMessages(target.stack, runtime, sessionContext.messages);
|
|
799
|
-
const diagnostics = [...target.diagnostics, ...system.diagnostics, ...messages.diagnostics];
|
|
800
|
-
const messageSections = messages.messages.map((message, index) => {
|
|
801
|
-
const content = agentMessageToPreviewText(message);
|
|
802
|
-
const source = messages.messageSources[index];
|
|
803
|
-
return previewSection(`message-${index}`, previewMessageTitle(source, index), content, message.role);
|
|
804
|
-
});
|
|
805
|
-
const systemSection = previewSection("system", "System prompt", system.systemPrompt || "(empty)");
|
|
806
|
-
const totalChars = systemSection.chars + messageSections.reduce((sum, section) => sum + section.chars, 0);
|
|
807
|
-
const preview: WebEditorPreview = {
|
|
808
|
-
stackId: target.stack.id,
|
|
809
|
-
generatedAt: new Date().toISOString(),
|
|
810
|
-
system: systemSection,
|
|
811
|
-
messages: messageSections,
|
|
812
|
-
totalChars,
|
|
813
|
-
approxTokens: estimatePayloadTokens(`${system.systemPrompt}\n${messageSections.map((section) => section.content).join("\n")}`),
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
const text = [
|
|
817
|
-
`# Prompt stack preview: ${target.stack.id}`,
|
|
818
|
-
"",
|
|
819
|
-
"## System prompt",
|
|
820
|
-
"",
|
|
821
|
-
system.systemPrompt || "(empty)",
|
|
822
|
-
"",
|
|
823
|
-
"## Message layout",
|
|
824
|
-
"",
|
|
825
|
-
renderPreviewSectionText(messageSections),
|
|
826
|
-
"",
|
|
827
|
-
"## Diagnostics",
|
|
828
|
-
"",
|
|
829
|
-
renderDiagnostics(diagnostics),
|
|
830
|
-
].join("\n");
|
|
831
|
-
|
|
832
|
-
return { text, preview, diagnostics };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function previewMessageTitle(source: CompileMessageSource | undefined, index: number): string {
|
|
836
|
-
if (source?.kind === "stack-item") {
|
|
837
|
-
return source.itemName?.trim() || source.itemId || `Stack item ${index + 1}`;
|
|
838
|
-
}
|
|
839
|
-
if (source?.kind === "chat-history") {
|
|
840
|
-
const label = source.itemName?.trim() || "Chat history";
|
|
841
|
-
return `${label} #${source.historyIndex ?? index + 1}`;
|
|
842
|
-
}
|
|
843
|
-
if (source?.kind === "implicit-history") {
|
|
844
|
-
return `Conversation history #${source.historyIndex ?? index + 1}`;
|
|
845
|
-
}
|
|
846
|
-
return `Message ${index + 1}`;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function renderPreviewSectionText(sections: WebEditorPreviewSection[], maxChars = 8000): string {
|
|
850
|
-
let text = "";
|
|
851
|
-
for (const section of sections) {
|
|
852
|
-
const role = section.role ? ` (${section.role})` : "";
|
|
853
|
-
text += `\n--- ${section.title}${role} ---\n`;
|
|
854
|
-
text += section.content;
|
|
855
|
-
text += "\n";
|
|
856
|
-
if (text.length > maxChars) return `${text.slice(0, maxChars)}\n\n[preview truncated]`;
|
|
857
|
-
}
|
|
858
|
-
return text.trimStart();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function previewSection(id: string, title: string, content: string, role?: string): WebEditorPreviewSection {
|
|
862
|
-
return {
|
|
863
|
-
id,
|
|
864
|
-
title,
|
|
865
|
-
role,
|
|
866
|
-
content,
|
|
867
|
-
chars: content.length,
|
|
868
|
-
approxTokens: estimatePayloadTokens(content),
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function renderDiagnostics(diagnostics: PromptStackDiagnostic[]): string {
|
|
873
|
-
if (diagnostics.length === 0) return "No diagnostics.";
|
|
874
|
-
return diagnostics.map((d) => `${d.level.toUpperCase()}${d.itemId ? ` [${d.itemId}]` : ""}: ${d.message}`).join("\n");
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async function showText(ctx: ExtensionCommandContext, title: string, text: string): Promise<void> {
|
|
878
|
-
if (ctx.hasUI) {
|
|
879
|
-
await ctx.ui.editor(title, text);
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
console.log(text);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function captureProviderPayload(value: unknown, savePath?: string): WebEditorPayloadCapture {
|
|
886
|
-
const capture = createProviderPayloadCapture(value, { stackId: active?.stack.id, savePath });
|
|
887
|
-
latestProviderPayloadCapture = capture;
|
|
888
|
-
return capture;
|
|
889
|
-
}
|
|
890
445
|
}
|