@zihanw/pi-forge 0.3.0 → 0.3.1
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 +80 -2
- package/README.zh-CN.md +65 -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/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +364 -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 +223 -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 +24 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +13 -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 +6 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +23 -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 +78 -0
- package/examples/custom-system-status-extension/index.ts +176 -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/index.ts +192 -652
- 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 +271 -0
- package/src/preview.ts +226 -0
- package/src/render-helpers.ts +169 -0
- package/src/runtime-state.ts +36 -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/types.ts +3 -14
- package/src/web-editor/page.ts +516 -30
- package/src/web-editor/server.ts +5 -0
- package/src/web-editor/types.ts +14 -0
- package/src/web-host.ts +3 -0
package/src/index.ts
CHANGED
|
@@ -1,31 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
compileMessages,
|
|
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 { registerLifecycleHandlers } from "./lifecycle.ts";
|
|
3
|
+
import { chooseDefaultStack, isDisabledPromptStackId, loadPromptStacks } from "./loader.ts";
|
|
4
|
+
import { registerPayloadCommands, registerPayloadRequestHandler, armPayloadIntercept, clearPayloadCapture, webPayloadSnapshot } from "./payload-command.ts";
|
|
15
5
|
import { applyResourcePolicy, hasResourcePolicy } from "./policy.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import type {
|
|
6
|
+
import { buildPreview, showText } from "./preview.ts";
|
|
7
|
+
import { registerPresetCommand, selectedActiveId as selectedActiveIdForState } from "./preset-command.ts";
|
|
8
|
+
import { createRuntimeState, STATE_ENTRY_TYPE } from "./runtime-state.ts";
|
|
9
|
+
import type { PromptStack, PromptStackDiagnostic } from "./types.ts";
|
|
20
10
|
import { createWebEditorHost, loadWebEditorSettings, type WebHostRuntime } from "./web-host.ts";
|
|
21
|
-
import { startWebEditorServer, type
|
|
11
|
+
import { startWebEditorServer, type WebEditorPolicyResource, type WebEditorPolicyResources, type WebEditorServer } from "./web-editor/index.ts";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
getRegisteredMacros,
|
|
15
|
+
registerMacro,
|
|
16
|
+
type PromptMacroDefinition,
|
|
17
|
+
type PromptMacroRenderContext,
|
|
18
|
+
type PromptMacroRenderer,
|
|
19
|
+
} from "./macro-engine.ts";
|
|
20
|
+
export {
|
|
21
|
+
getRegisteredSlots,
|
|
22
|
+
registerSlot,
|
|
23
|
+
type PromptSlotDefinition,
|
|
24
|
+
type PromptSlotRenderContext,
|
|
25
|
+
type PromptSlotRenderer,
|
|
26
|
+
} from "./slot-renderers.ts";
|
|
27
|
+
export {
|
|
28
|
+
type PromptExtensionArgumentDefinition,
|
|
29
|
+
type PromptExtensionOptionDefinition,
|
|
30
|
+
type PromptExtensionOptionsSchema,
|
|
31
|
+
type PromptExtensionOptionType,
|
|
32
|
+
type PromptRegistryEntry,
|
|
33
|
+
} from "./extension-registry.ts";
|
|
34
|
+
export {
|
|
35
|
+
createVariableAccess,
|
|
36
|
+
promptRenderHelpers,
|
|
37
|
+
type PromptRenderHelpers,
|
|
38
|
+
type PromptVariableAccess,
|
|
39
|
+
type PromptVariableScope,
|
|
40
|
+
type PromptWritableVariableScope,
|
|
41
|
+
} from "./render-helpers.ts";
|
|
22
42
|
|
|
23
|
-
const STATE_ENTRY_TYPE = "pi-forge-prompt-stack-state";
|
|
24
|
-
const VARIABLE_ENTRY_TYPE = "pi-forge-variable-state";
|
|
25
43
|
const WEB_EDITOR_GLOBAL_KEY = "__piForgeWebEditor";
|
|
26
44
|
|
|
27
|
-
type PayloadDisplayTarget = "editor" | "web";
|
|
28
|
-
|
|
29
45
|
interface SharedWebEditorState {
|
|
30
46
|
server?: WebEditorServer;
|
|
31
47
|
cwd?: string;
|
|
@@ -48,52 +64,38 @@ function getSharedWebEditorRegistry(): SharedWebEditorRegistry {
|
|
|
48
64
|
|
|
49
65
|
export default function piForge(pi: ExtensionAPI) {
|
|
50
66
|
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[] = [];
|
|
67
|
+
const state = createRuntimeState();
|
|
65
68
|
let webEditor: WebEditorServer | undefined;
|
|
66
69
|
let webEditorCwd: string | undefined;
|
|
67
70
|
let webEditorPreferredPort: number | undefined;
|
|
68
71
|
let toolPolicyBaseline: string[] | undefined;
|
|
69
72
|
|
|
70
73
|
function activeId(): string | undefined {
|
|
71
|
-
return active?.stack.id;
|
|
74
|
+
return state.active?.stack.id;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
function selectedActiveId(): string | undefined {
|
|
75
|
-
|
|
76
|
-
return isDisabledPromptStackId(lastPersistedActiveId) ? "none" : undefined;
|
|
78
|
+
return selectedActiveIdForState(state);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function persistActiveSelection(id: string): void {
|
|
80
|
-
if (id === lastPersistedActiveId) return;
|
|
82
|
+
if (id === state.lastPersistedActiveId) return;
|
|
81
83
|
pi.appendEntry(STATE_ENTRY_TYPE, { activeStackId: id });
|
|
82
|
-
lastPersistedActiveId = id;
|
|
84
|
+
state.lastPersistedActiveId = id;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
function setActive(id: string | undefined, ctx?: ExtensionContext): boolean {
|
|
86
88
|
if (!id || isDisabledPromptStackId(id)) {
|
|
87
|
-
active = undefined;
|
|
89
|
+
state.active = undefined;
|
|
88
90
|
if (id) persistActiveSelection("none");
|
|
89
91
|
if (ctx) updateStatus(ctx);
|
|
90
92
|
syncActiveToolPolicy(ctx);
|
|
91
93
|
return true;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
const found = stacks.find((candidate) => candidate.stack.id === id);
|
|
96
|
+
const found = state.stacks.find((candidate) => candidate.stack.id === id);
|
|
95
97
|
if (!found) return false;
|
|
96
|
-
active = found;
|
|
98
|
+
state.active = found;
|
|
97
99
|
persistActiveSelection(found.stack.id);
|
|
98
100
|
if (ctx) updateStatus(ctx);
|
|
99
101
|
syncActiveToolPolicy(ctx);
|
|
@@ -102,32 +104,32 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
102
104
|
|
|
103
105
|
function reloadStacks(ctx: ExtensionContext, preferredId?: string): void {
|
|
104
106
|
if (!ctx.isProjectTrusted()) {
|
|
105
|
-
stacks = [];
|
|
106
|
-
active = undefined;
|
|
107
|
+
state.stacks = [];
|
|
108
|
+
state.active = undefined;
|
|
107
109
|
syncActiveToolPolicy(ctx);
|
|
108
110
|
ctx.ui.notify("pi-forge: project is not trusted; prompt stacks are disabled.", "warning");
|
|
109
111
|
updateStatus(ctx);
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
stacks = loadPromptStacks(ctx.cwd);
|
|
114
|
-
active = chooseDefaultStack(stacks, preferredId);
|
|
115
|
+
state.stacks = loadPromptStacks(ctx.cwd);
|
|
116
|
+
state.active = chooseDefaultStack(state.stacks, preferredId);
|
|
115
117
|
updateStatus(ctx);
|
|
116
118
|
syncActiveToolPolicy(ctx);
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
function updateStatus(ctx: ExtensionContext): void {
|
|
120
|
-
if (active) {
|
|
121
|
-
ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + active.stack.id));
|
|
122
|
+
if (state.active) {
|
|
123
|
+
ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + state.active.stack.id));
|
|
122
124
|
} else {
|
|
123
125
|
ctx.ui.setStatus("pi-forge", undefined);
|
|
124
|
-
latestCompileDiagnostics = [];
|
|
126
|
+
state.latestCompileDiagnostics = [];
|
|
125
127
|
ctx.ui.setStatus("pi-forge-diagnostics", undefined);
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
function syncActiveToolPolicy(ctx?: ExtensionContext): void {
|
|
130
|
-
const policy = active?.stack.tools;
|
|
132
|
+
const policy = state.active?.stack.tools;
|
|
131
133
|
if (!hasResourcePolicy(policy)) {
|
|
132
134
|
restoreToolPolicy(ctx);
|
|
133
135
|
return;
|
|
@@ -158,15 +160,15 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
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;
|
|
163
|
+
if (!state.active) return;
|
|
164
|
+
const errorCount = state.active.diagnostics.filter((d) => d.level === "error").length;
|
|
165
|
+
const warningCount = state.active.diagnostics.filter((d) => d.level === "warning").length;
|
|
164
166
|
const suffix = errorCount || warningCount ? " (" + errorCount + " errors, " + warningCount + " warnings)" : "";
|
|
165
|
-
ctx.ui.notify("pi-forge: active preset " + active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
|
|
167
|
+
ctx.ui.notify("pi-forge: active preset " + state.active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function recordCompileDiagnostics(ctx: ExtensionContext, diagnostics: PromptStackDiagnostic[]): void {
|
|
169
|
-
latestCompileDiagnostics = diagnostics;
|
|
171
|
+
state.latestCompileDiagnostics = diagnostics;
|
|
170
172
|
const errors = diagnostics.filter((d) => d.level === "error").length;
|
|
171
173
|
const warnings = diagnostics.filter((d) => d.level === "warning").length;
|
|
172
174
|
if (errors || warnings) {
|
|
@@ -178,25 +180,133 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
178
180
|
|
|
179
181
|
function webHostRuntime(ctx: ExtensionCommandContext): WebHostRuntime {
|
|
180
182
|
return {
|
|
181
|
-
getStacks: () => stacks,
|
|
182
|
-
getActive: () => active,
|
|
183
|
+
getStacks: () => state.stacks,
|
|
184
|
+
getActive: () => state.active,
|
|
183
185
|
getActiveId: activeId,
|
|
184
186
|
getSelectedActiveId: selectedActiveId,
|
|
185
187
|
setActive: (id) => setActive(id, ctx),
|
|
186
188
|
reloadStacks: (preferredId) => reloadStacks(ctx, preferredId),
|
|
187
|
-
buildPreview: (target) => buildPreview(ctx, target),
|
|
188
|
-
|
|
189
|
+
buildPreview: (target) => buildPreview(ctx, target, state.sessionVariables, previewOptionsForStack(ctx, target.stack)),
|
|
190
|
+
getPolicyResources: () => getPolicyResources(ctx),
|
|
191
|
+
getPayload: () => ({ ok: true, ...webPayloadSnapshot(state) }),
|
|
189
192
|
armPayload: (savePath) => {
|
|
190
|
-
armPayloadIntercept(ctx, savePath, "web");
|
|
191
|
-
return { ok: true, ...webPayloadSnapshot() };
|
|
193
|
+
armPayloadIntercept(state, ctx, savePath, "web");
|
|
194
|
+
return { ok: true, ...webPayloadSnapshot(state) };
|
|
192
195
|
},
|
|
193
196
|
clearPayload: () => {
|
|
194
|
-
clearPayloadCapture(ctx);
|
|
195
|
-
return { ok: true, ...webPayloadSnapshot() };
|
|
197
|
+
clearPayloadCapture(state, ctx);
|
|
198
|
+
return { ok: true, ...webPayloadSnapshot(state) };
|
|
196
199
|
},
|
|
197
200
|
};
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
function getPolicyResources(ctx: ExtensionCommandContext): WebEditorPolicyResources {
|
|
204
|
+
const options = ctx.getSystemPromptOptions();
|
|
205
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
206
|
+
const snippets = options.toolSnippets ?? {};
|
|
207
|
+
const tools = pi.getAllTools()
|
|
208
|
+
.map((tool) => normalizeToolResource(tool, activeTools, snippets))
|
|
209
|
+
.filter(hasPolicyResourceName)
|
|
210
|
+
.sort(comparePolicyResource);
|
|
211
|
+
const skills = (options.skills ?? [])
|
|
212
|
+
.map(normalizeSkillResource)
|
|
213
|
+
.filter(hasPolicyResourceName)
|
|
214
|
+
.sort(comparePolicyResource);
|
|
215
|
+
return { tools, skills };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function previewOptionsForStack(ctx: ExtensionCommandContext, stack: PromptStack): BuildSystemPromptOptions {
|
|
219
|
+
const base = ctx.getSystemPromptOptions();
|
|
220
|
+
const baseSelectedTools = Array.isArray(base.selectedTools) ? base.selectedTools : pi.getActiveTools();
|
|
221
|
+
const policyActive = hasResourcePolicy(stack.tools);
|
|
222
|
+
const baselineTools = policyActive ? (toolPolicyBaseline ?? pi.getActiveTools()) : baseSelectedTools;
|
|
223
|
+
const selectedTools = policyActive
|
|
224
|
+
? applyResourcePolicy(filterKnownTools(baselineTools), stack.tools)
|
|
225
|
+
: baseSelectedTools;
|
|
226
|
+
const selectedToolSet = new Set(selectedTools);
|
|
227
|
+
const toolSnippets = filterToolSnippets(base.toolSnippets ?? {}, selectedToolSet);
|
|
228
|
+
const toolInfos = pi.getAllTools();
|
|
229
|
+
for (const tool of toolInfos) {
|
|
230
|
+
const name = stringValue((tool as { name?: unknown }).name);
|
|
231
|
+
if (!name || !selectedToolSet.has(name) || toolSnippets[name]) continue;
|
|
232
|
+
const snippet = stringValue((tool as { promptSnippet?: unknown }).promptSnippet);
|
|
233
|
+
if (snippet) toolSnippets[name] = snippet;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const mappedGuidelines = toolInfos
|
|
237
|
+
.filter((tool) => {
|
|
238
|
+
const name = stringValue((tool as { name?: unknown }).name);
|
|
239
|
+
return !!name && selectedToolSet.has(name);
|
|
240
|
+
})
|
|
241
|
+
.flatMap((tool) => stringArrayValue((tool as { promptGuidelines?: unknown }).promptGuidelines));
|
|
242
|
+
const promptGuidelines = policyActive && !sameStringSet(baseSelectedTools, selectedTools)
|
|
243
|
+
? mappedGuidelines
|
|
244
|
+
: (base.promptGuidelines ?? mappedGuidelines);
|
|
245
|
+
|
|
246
|
+
return { ...base, selectedTools, toolSnippets, promptGuidelines };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function filterToolSnippets(snippets: Record<string, string | undefined>, selectedTools: Set<string>): Record<string, string> {
|
|
250
|
+
const filtered: Record<string, string> = {};
|
|
251
|
+
for (const [name, snippet] of Object.entries(snippets)) {
|
|
252
|
+
if (selectedTools.has(name) && snippet) filtered[name] = snippet;
|
|
253
|
+
}
|
|
254
|
+
return filtered;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeToolResource(
|
|
258
|
+
tool: { name?: unknown; description?: unknown; promptSnippet?: unknown; sourceInfo?: unknown },
|
|
259
|
+
activeTools: Set<string>,
|
|
260
|
+
snippets: Record<string, string | undefined>,
|
|
261
|
+
): WebEditorPolicyResource {
|
|
262
|
+
const name = String(tool.name ?? "");
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
description: stringValue(tool.description) ?? stringValue(tool.promptSnippet) ?? snippets[name],
|
|
266
|
+
source: sourceLabel(tool.sourceInfo),
|
|
267
|
+
active: activeTools.has(name),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizeSkillResource(skill: { name?: unknown; description?: unknown; filePath?: unknown; disableModelInvocation?: unknown }): WebEditorPolicyResource {
|
|
272
|
+
return {
|
|
273
|
+
name: String(skill.name ?? ""),
|
|
274
|
+
description: stringValue(skill.description),
|
|
275
|
+
source: stringValue(skill.filePath),
|
|
276
|
+
hidden: skill.disableModelInvocation === true,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stringValue(value: unknown): string | undefined {
|
|
281
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function stringArrayValue(value: unknown): string[] {
|
|
285
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && !!item.trim()) : [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sourceLabel(value: unknown): string | undefined {
|
|
289
|
+
if (!value || typeof value !== "object") return undefined;
|
|
290
|
+
const source = stringValue((value as { source?: unknown }).source);
|
|
291
|
+
const path = stringValue((value as { path?: unknown }).path);
|
|
292
|
+
if (source && path) return `${source}: ${path}`;
|
|
293
|
+
return source ?? path;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function comparePolicyResource(a: WebEditorPolicyResource, b: WebEditorPolicyResource): number {
|
|
297
|
+
return a.name.localeCompare(b.name);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasPolicyResourceName(resource: WebEditorPolicyResource): boolean {
|
|
301
|
+
return !!resource.name.trim();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sameStringSet(a: string[], b: string[]): boolean {
|
|
305
|
+
if (a.length !== b.length) return false;
|
|
306
|
+
const bSet = new Set(b);
|
|
307
|
+
return a.every((value) => bSet.has(value));
|
|
308
|
+
}
|
|
309
|
+
|
|
200
310
|
function sharedWebEditorForCwd(cwd: string): SharedWebEditorState {
|
|
201
311
|
sharedWebEditors.byCwd[cwd] ??= {};
|
|
202
312
|
return sharedWebEditors.byCwd[cwd];
|
|
@@ -299,592 +409,22 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
299
409
|
ctx.ui.notify("pi-forge: stack editor stopped.", "info");
|
|
300
410
|
}
|
|
301
411
|
|
|
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
|
-
},
|
|
412
|
+
registerLifecycleHandlers(pi, state, {
|
|
413
|
+
reloadStacks,
|
|
414
|
+
refreshWebEditorHost,
|
|
415
|
+
notifyActivePreset,
|
|
416
|
+
syncActiveToolPolicy,
|
|
417
|
+
activeId,
|
|
418
|
+
persistActiveSelection,
|
|
419
|
+
recordCompileDiagnostics,
|
|
537
420
|
});
|
|
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
|
-
},
|
|
421
|
+
registerPayloadRequestHandler(pi, state, () => state.active);
|
|
422
|
+
registerPayloadCommands(pi, state);
|
|
423
|
+
registerPresetCommand(pi, state, {
|
|
424
|
+
selectedActiveId,
|
|
425
|
+
setActive,
|
|
426
|
+
reloadStacks,
|
|
427
|
+
openWebEditor,
|
|
428
|
+
stopWebEditor,
|
|
568
429
|
});
|
|
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
430
|
}
|