@firstpick/pi-utils 0.1.7 → 0.1.8
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/index.ts +1 -0
- package/package.json +2 -1
- package/src/prompt-export-estimate.ts +198 -0
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./src/env";
|
|
|
3
3
|
export * from "./src/text";
|
|
4
4
|
export * from "./src/tokens";
|
|
5
5
|
export * from "./src/prompt-calibration";
|
|
6
|
+
export * from "./src/prompt-export-estimate";
|
|
6
7
|
export * from "./src/async";
|
|
7
8
|
export * from "./src/ui/working-indicator";
|
|
8
9
|
export * from "./src/local-wiki";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Shared utilities for Firstpick Pi extension packages.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"./text": "./src/text.ts",
|
|
11
11
|
"./tokens": "./src/tokens.ts",
|
|
12
12
|
"./prompt-calibration": "./src/prompt-calibration.ts",
|
|
13
|
+
"./prompt-export-estimate": "./src/prompt-export-estimate.ts",
|
|
13
14
|
"./async": "./src/async.ts",
|
|
14
15
|
"./ui": "./src/ui/working-indicator.ts",
|
|
15
16
|
"./local-wiki": "./src/local-wiki.ts"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { InitialPromptCalibration, InitialPromptInputEstimate, InitialPromptToolInfo } from "./tokens";
|
|
8
|
+
import { estimateInitialPromptInput } from "./tokens";
|
|
9
|
+
|
|
10
|
+
export type ExportBackedInitialPromptEstimate = {
|
|
11
|
+
estimate: InitialPromptInputEstimate;
|
|
12
|
+
systemPrompt: string;
|
|
13
|
+
/** Active tool schemas used for the estimate, preferably decoded from the temporary export HTML. */
|
|
14
|
+
tools: InitialPromptToolInfo[];
|
|
15
|
+
source: "export-html" | "direct";
|
|
16
|
+
warning?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ExportSessionData = {
|
|
20
|
+
systemPrompt?: unknown;
|
|
21
|
+
tools?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ExportSessionToHtml = (
|
|
25
|
+
sm: ExtensionContext["sessionManager"],
|
|
26
|
+
state?: { systemPrompt?: string; tools?: InitialPromptToolInfo[] },
|
|
27
|
+
options?: { outputPath?: string; themeName?: string } | string,
|
|
28
|
+
) => Promise<string>;
|
|
29
|
+
|
|
30
|
+
type PromptEstimatePiApi = Pick<ExtensionAPI, "getActiveTools" | "getAllTools">;
|
|
31
|
+
type PromptEstimateContext = Pick<ExtensionContext, "getSystemPrompt" | "sessionManager">;
|
|
32
|
+
|
|
33
|
+
let exportSessionToHtmlPromise: Promise<ExportSessionToHtml | null> | null = null;
|
|
34
|
+
|
|
35
|
+
function resolvePiExportHtmlModuleUrl(): string | null {
|
|
36
|
+
try {
|
|
37
|
+
const basePath = typeof __filename === "string" && path.isAbsolute(__filename) ? __filename : path.join(process.cwd(), "package.json");
|
|
38
|
+
const requireFromHere = createRequire(basePath);
|
|
39
|
+
const candidateDirs = requireFromHere.resolve.paths("@earendil-works/pi-coding-agent") ?? [];
|
|
40
|
+
for (const dir of candidateDirs) {
|
|
41
|
+
const candidate = path.join(dir, "@earendil-works", "pi-coding-agent", "dist", "core", "export-html", "index.js");
|
|
42
|
+
if (fs.existsSync(candidate)) return pathToFileURL(candidate).href;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadPiExportSessionToHtml(): Promise<ExportSessionToHtml | null> {
|
|
51
|
+
exportSessionToHtmlPromise ??= (async () => {
|
|
52
|
+
try {
|
|
53
|
+
const exportModuleUrl = resolvePiExportHtmlModuleUrl();
|
|
54
|
+
if (!exportModuleUrl) return null;
|
|
55
|
+
const mod = (await import(exportModuleUrl)) as { exportSessionToHtml?: unknown };
|
|
56
|
+
return typeof mod.exportSessionToHtml === "function" ? (mod.exportSessionToHtml as ExportSessionToHtml) : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return exportSessionToHtmlPromise;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeTempExportPath(sessionId: string | undefined): string {
|
|
65
|
+
const safeSessionId = (sessionId || "session").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
66
|
+
const nonce = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
67
|
+
return path.join(os.tmpdir(), `pi-prompt-estimate-export-${safeSessionId}-${nonce}.html`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function decodeSessionDataFromExportHtml(html: string): ExportSessionData | null {
|
|
71
|
+
const match = html.match(/<script[^>]*id=["']session-data["'][^>]*>([^<]*)<\/script>/i);
|
|
72
|
+
const encoded = match?.[1]?.trim();
|
|
73
|
+
if (!encoded) return null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
77
|
+
return parsed && typeof parsed === "object" ? (parsed as ExportSessionData) : null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeExportedTools(tools: unknown): InitialPromptToolInfo[] {
|
|
84
|
+
if (!Array.isArray(tools)) return [];
|
|
85
|
+
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
const normalized: InitialPromptToolInfo[] = [];
|
|
88
|
+
for (const tool of tools) {
|
|
89
|
+
const record = (tool && typeof tool === "object" ? tool : {}) as Record<string, unknown>;
|
|
90
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
91
|
+
if (!name || seen.has(name)) continue;
|
|
92
|
+
seen.add(name);
|
|
93
|
+
normalized.push({
|
|
94
|
+
name,
|
|
95
|
+
description: typeof record.description === "string" ? record.description : undefined,
|
|
96
|
+
parameters: record.parameters,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getActiveInitialPromptToolInfos(pi: PromptEstimatePiApi): InitialPromptToolInfo[] {
|
|
103
|
+
let activeTools: string[] = [];
|
|
104
|
+
let allTools: InitialPromptToolInfo[] = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
activeTools = pi.getActiveTools();
|
|
108
|
+
} catch {
|
|
109
|
+
activeTools = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
allTools = pi.getAllTools().map((tool) => ({
|
|
114
|
+
name: tool.name,
|
|
115
|
+
description: tool.description,
|
|
116
|
+
parameters: tool.parameters,
|
|
117
|
+
}));
|
|
118
|
+
} catch {
|
|
119
|
+
allTools = [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (allTools.length === 0) return [];
|
|
123
|
+
|
|
124
|
+
const toolsByName = new Map<string, InitialPromptToolInfo>();
|
|
125
|
+
for (const tool of allTools) {
|
|
126
|
+
if (tool.name && !toolsByName.has(tool.name)) toolsByName.set(tool.name, tool);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const orderedNames = activeTools.length > 0 ? activeTools : Array.from(toolsByName.keys()).sort();
|
|
130
|
+
return orderedNames.map((name) => toolsByName.get(name)).filter((tool): tool is InitialPromptToolInfo => !!tool);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function estimateInitialPromptForPiContext(
|
|
134
|
+
pi: PromptEstimatePiApi,
|
|
135
|
+
systemPrompt: string,
|
|
136
|
+
calibration?: InitialPromptCalibration | null,
|
|
137
|
+
exportedTools?: InitialPromptToolInfo[],
|
|
138
|
+
): InitialPromptInputEstimate {
|
|
139
|
+
const tools = exportedTools ?? getActiveInitialPromptToolInfos(pi);
|
|
140
|
+
return estimateInitialPromptInput({
|
|
141
|
+
systemPrompt,
|
|
142
|
+
activeTools: tools.map((tool) => tool.name),
|
|
143
|
+
allTools: tools,
|
|
144
|
+
calibration,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function estimateInitialPromptFromPiExport(
|
|
149
|
+
pi: PromptEstimatePiApi,
|
|
150
|
+
ctx: PromptEstimateContext,
|
|
151
|
+
calibration?: InitialPromptCalibration | null,
|
|
152
|
+
): Promise<ExportBackedInitialPromptEstimate> {
|
|
153
|
+
const fallbackSystemPrompt = ctx.getSystemPrompt();
|
|
154
|
+
const fallbackTools = getActiveInitialPromptToolInfos(pi);
|
|
155
|
+
const exportSessionToHtml = await loadPiExportSessionToHtml();
|
|
156
|
+
if (!exportSessionToHtml) {
|
|
157
|
+
return {
|
|
158
|
+
estimate: estimateInitialPromptForPiContext(pi, fallbackSystemPrompt, calibration, fallbackTools),
|
|
159
|
+
systemPrompt: fallbackSystemPrompt,
|
|
160
|
+
tools: fallbackTools,
|
|
161
|
+
source: "direct",
|
|
162
|
+
warning: "Pi HTML export API unavailable; used live context fallback.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const outputPath = makeTempExportPath(ctx.sessionManager.getSessionId());
|
|
167
|
+
let writtenPath = outputPath;
|
|
168
|
+
try {
|
|
169
|
+
writtenPath = await exportSessionToHtml(
|
|
170
|
+
ctx.sessionManager,
|
|
171
|
+
{ systemPrompt: fallbackSystemPrompt, tools: fallbackTools },
|
|
172
|
+
{ outputPath },
|
|
173
|
+
);
|
|
174
|
+
const html = fs.readFileSync(writtenPath, "utf8");
|
|
175
|
+
const sessionData = decodeSessionDataFromExportHtml(html);
|
|
176
|
+
const exportedSystemPrompt = typeof sessionData?.systemPrompt === "string" ? sessionData.systemPrompt : fallbackSystemPrompt;
|
|
177
|
+
const exportedTools = normalizeExportedTools(sessionData?.tools);
|
|
178
|
+
const tools = exportedTools.length > 0 ? exportedTools : fallbackTools;
|
|
179
|
+
const estimate = estimateInitialPromptForPiContext(pi, exportedSystemPrompt, calibration, tools);
|
|
180
|
+
return { estimate, systemPrompt: exportedSystemPrompt, tools, source: "export-html" };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
estimate: estimateInitialPromptForPiContext(pi, fallbackSystemPrompt, calibration, fallbackTools),
|
|
184
|
+
systemPrompt: fallbackSystemPrompt,
|
|
185
|
+
tools: fallbackTools,
|
|
186
|
+
source: "direct",
|
|
187
|
+
warning: `Pi HTML export failed; used live context fallback (${error instanceof Error ? error.message : String(error)}).`,
|
|
188
|
+
};
|
|
189
|
+
} finally {
|
|
190
|
+
for (const filePath of new Set([outputPath, writtenPath])) {
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(filePath, { force: true });
|
|
193
|
+
} catch {
|
|
194
|
+
// Best-effort cleanup only.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|