@firstpick/pi-extension-stats 0.1.9 → 0.2.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 +6 -5
- package/images/stats_v0.1.2.png +0 -0
- package/index.ts +279 -16
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@ No required configuration.
|
|
|
26
26
|
|
|
27
27
|
- `/stats [days|all]` — show token usage dashboard (default: last 14 days).
|
|
28
28
|
- `/stats tokens` — show current context token breakdown by source/type.
|
|
29
|
-
- `/stats-pi` — show estimated initial prompt input token breakdown. It counts Pi's system prompt text, active provider-level tool schemas, framing overhead, and optional historical calibration.
|
|
29
|
+
- `/stats-pi` — show export-backed estimated initial prompt input token breakdown. It creates a temporary Pi HTML export, decodes its embedded session data, then counts Pi's system prompt text, active provider-level tool schemas, framing overhead, and optional historical calibration (falling back to live context data if export is unavailable).
|
|
30
|
+
- `/stats-pi detailed` — add a concise detail view of the exported initial prompt snapshot: active tool schemas, available-tool prompt entries, skills, context files, metadata, and estimate components.
|
|
30
31
|
- `/calibrate` — start an isolated calibration session with a fixed probe prompt, then update `/stats-pi` and the footer `PI: X tok` estimate from the first assistant response usage. `/calibrate current` reuses the current branch if it already has a suitable first-turn usage sample.
|
|
31
32
|
- `/stats-last [days|all]` — show non-zero daily usage graph.
|
|
32
33
|
- `/stats-most-expense [days|all]` — show most expensive sessions.
|
|
@@ -36,13 +37,13 @@ No required configuration.
|
|
|
36
37
|
|
|
37
38
|
## Prompt input estimate
|
|
38
39
|
|
|
39
|
-
`/stats-pi` and the `PI: ~X tok` value in `/stats` estimate the full initial model input, not just raw prompt text. `/stats-pi` can be run before any LLM prompt in a fresh session.
|
|
40
|
+
`/stats-pi` and the `PI: ~X tok` value in `/stats` estimate the full initial model input, not just raw prompt text. `/stats-pi` prefers Pi's own HTML export data for the exact exported system prompt and active tool definitions; it falls back to live context data when a temporary export cannot be produced, so it can still be run before any LLM prompt in a fresh session.
|
|
40
41
|
|
|
41
|
-
The calculation is intentionally provider-agnostic:
|
|
42
|
+
The token calculation is intentionally provider-agnostic:
|
|
42
43
|
|
|
43
44
|
```text
|
|
44
|
-
promptTextTokens = weighted text estimate of
|
|
45
|
-
toolSchemaTokens = weighted text estimate of active tool definitions JSON
|
|
45
|
+
promptTextTokens = weighted text estimate of the system prompt (from exported session data when available)
|
|
46
|
+
toolSchemaTokens = weighted text estimate of active tool definitions JSON (from exported session data when available)
|
|
46
47
|
framingTokens = conservative message/request framing allowance
|
|
47
48
|
baseEstimate = promptTextTokens + toolSchemaTokens + framingTokens
|
|
48
49
|
estimatedInitialInput = baseEstimate × historicalCalibrationMultiplier
|
|
Binary file
|
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
appendInitialPromptCalibrationRecord,
|
|
7
7
|
buildInitialPromptCalibrationRecord,
|
|
8
8
|
collectInitialPromptCalibration,
|
|
9
|
+
estimateInitialPromptFromPiExport,
|
|
9
10
|
estimateInitialPromptInput,
|
|
10
11
|
estimatePromptInjectionTokens,
|
|
11
12
|
estimateTokensFromCharCount,
|
|
@@ -195,12 +196,6 @@ function formatTokenCell(tokens: number): string {
|
|
|
195
196
|
return tokens < 0 ? `-${formatTokens(Math.abs(tokens))}` : formatTokens(tokens);
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
function formatCalibrationSummary(estimate: InitialPromptInputEstimate): string {
|
|
199
|
-
if (estimate.calibrationSamples <= 0) return "uncalibrated";
|
|
200
|
-
const sampleLabel = estimate.calibrationSamples === 1 ? "sample" : "samples";
|
|
201
|
-
return `learned scale ×${estimate.calibrationMultiplier.toFixed(2)} from ${estimate.calibrationSamples} ${sampleLabel}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
199
|
function distributeCalibratedTokens<T extends { tokens: number }>(sources: T[], calibratedTotal: number): T[] {
|
|
205
200
|
const uncalibratedTotal = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
206
201
|
if (uncalibratedTotal <= 0 || calibratedTotal <= 0) return sources.map((source) => ({ ...source, tokens: 0 }));
|
|
@@ -220,7 +215,12 @@ function distributeCalibratedTokens<T extends { tokens: number }>(sources: T[],
|
|
|
220
215
|
return sources.map((source, index) => ({ ...source, tokens: exact[index]?.tokens ?? 0 }));
|
|
221
216
|
}
|
|
222
217
|
|
|
223
|
-
function formatPromptInjectionLines(
|
|
218
|
+
function formatPromptInjectionLines(
|
|
219
|
+
systemPrompt: string,
|
|
220
|
+
options: BuildSystemPromptOptions | null,
|
|
221
|
+
estimate: InitialPromptInputEstimate,
|
|
222
|
+
metadata?: { source?: string; warning?: string },
|
|
223
|
+
): string[] {
|
|
224
224
|
const promptSources = buildPromptInjectionSources(systemPrompt, options)
|
|
225
225
|
.map((source) => ({
|
|
226
226
|
...source,
|
|
@@ -247,9 +247,16 @@ function formatPromptInjectionLines(systemPrompt: string, options: BuildSystemPr
|
|
|
247
247
|
});
|
|
248
248
|
const range = estimate.low !== estimate.high ? ` · range ${formatTokens(estimate.low)}–${formatTokens(estimate.high)}` : "";
|
|
249
249
|
|
|
250
|
+
const metadataLines = [
|
|
251
|
+
metadata?.source ? `Source: ${metadata.source}` : null,
|
|
252
|
+
metadata?.warning ? `Note: ${metadata.warning}` : null,
|
|
253
|
+
].filter((line): line is string => !!line);
|
|
254
|
+
|
|
255
|
+
const confidenceLabel = estimate.confidence === "measured-after-call" ? "measured" : `${estimate.confidence} estimate`;
|
|
256
|
+
|
|
250
257
|
return [
|
|
251
|
-
`
|
|
252
|
-
|
|
258
|
+
`PI initial input: ~${formatTokens(estimate.total)} tok (${confidenceLabel}${range})`,
|
|
259
|
+
...metadataLines,
|
|
253
260
|
`┌${"─".repeat(labelWidth + 2)}┬${"─".repeat(tokenWidth + 2)}┬${"─".repeat(percentWidth + 6)}┐`,
|
|
254
261
|
`│ ${"Source".padEnd(labelWidth)} │ ${"Tokens".padStart(tokenWidth)} │ ${"%".padStart(percentWidth + 4)} │`,
|
|
255
262
|
separator,
|
|
@@ -259,6 +266,240 @@ function formatPromptInjectionLines(systemPrompt: string, options: BuildSystemPr
|
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
|
|
269
|
+
type PromptSkillDetail = {
|
|
270
|
+
name: string;
|
|
271
|
+
description?: string;
|
|
272
|
+
location?: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
type ToolPromptEntry = {
|
|
276
|
+
name: string;
|
|
277
|
+
snippet: string;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
type ContextFileDetail = {
|
|
281
|
+
path: string;
|
|
282
|
+
chars?: number;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
function shortenText(text: string | undefined, maxLength = 90): string {
|
|
286
|
+
const normalized = (text ?? "").replace(/\s+/g, " ").trim();
|
|
287
|
+
if (normalized.length <= maxLength) return normalized;
|
|
288
|
+
return `${normalized.slice(0, maxLength - 1).trimEnd()}…`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatCountedNames(names: string[], limit = 18): string {
|
|
292
|
+
if (names.length === 0) return "none";
|
|
293
|
+
const shown = names.slice(0, limit).join(", ");
|
|
294
|
+
const remaining = names.length - limit;
|
|
295
|
+
return remaining > 0 ? `${shown}, … +${remaining} more` : shown;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function xmlUnescape(value: string): string {
|
|
299
|
+
return value
|
|
300
|
+
.replace(/</g, "<")
|
|
301
|
+
.replace(/>/g, ">")
|
|
302
|
+
.replace(/"/g, '"')
|
|
303
|
+
.replace(/'/g, "'")
|
|
304
|
+
.replace(/&/g, "&");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function extractXmlTag(block: string, tag: string): string | undefined {
|
|
308
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
309
|
+
const match = block.match(new RegExp(`<${escapedTag}>([\\s\\S]*?)<\\/${escapedTag}>`, "i"));
|
|
310
|
+
const value = match?.[1]?.trim();
|
|
311
|
+
return value ? xmlUnescape(value) : undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function extractAvailableToolPromptEntries(systemPrompt: string): ToolPromptEntry[] {
|
|
315
|
+
const marker = "Available tools:\n";
|
|
316
|
+
const start = systemPrompt.indexOf(marker);
|
|
317
|
+
if (start < 0) return [];
|
|
318
|
+
|
|
319
|
+
const tail = systemPrompt.slice(start + marker.length);
|
|
320
|
+
const blockEnd = tail.search(/\n\n/);
|
|
321
|
+
const block = blockEnd >= 0 ? tail.slice(0, blockEnd) : tail;
|
|
322
|
+
const entries: ToolPromptEntry[] = [];
|
|
323
|
+
const seen = new Set<string>();
|
|
324
|
+
|
|
325
|
+
for (const line of block.split(/\r?\n/)) {
|
|
326
|
+
const match = line.match(/^-\s+([^:\s]+):\s*(.*)$/);
|
|
327
|
+
const name = match?.[1]?.trim();
|
|
328
|
+
if (!name || seen.has(name)) continue;
|
|
329
|
+
seen.add(name);
|
|
330
|
+
entries.push({ name, snippet: match?.[2]?.trim() ?? "" });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return entries;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function extractPromptSkills(systemPrompt: string, options: BuildSystemPromptOptions | null): PromptSkillDetail[] {
|
|
337
|
+
const blockMatch = systemPrompt.match(/<available_skills>([\s\S]*?)<\/available_skills>/i);
|
|
338
|
+
const block = blockMatch?.[1];
|
|
339
|
+
if (block) {
|
|
340
|
+
const skills: PromptSkillDetail[] = [];
|
|
341
|
+
for (const match of block.matchAll(/<skill>([\s\S]*?)<\/skill>/gi)) {
|
|
342
|
+
const skillBlock = match[1] ?? "";
|
|
343
|
+
const name = extractXmlTag(skillBlock, "name");
|
|
344
|
+
if (!name) continue;
|
|
345
|
+
skills.push({
|
|
346
|
+
name,
|
|
347
|
+
description: extractXmlTag(skillBlock, "description"),
|
|
348
|
+
location: extractXmlTag(skillBlock, "location"),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (skills.length > 0) return skills;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (options?.skills ?? [])
|
|
355
|
+
.filter((skill) => !skill.disableModelInvocation)
|
|
356
|
+
.map((skill) => ({ name: skill.name, description: skill.description, location: skill.filePath }));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function extractPromptContextFiles(systemPrompt: string, options: BuildSystemPromptOptions | null): ContextFileDetail[] {
|
|
360
|
+
if (options?.contextFiles && options.contextFiles.length > 0) {
|
|
361
|
+
return options.contextFiles.map((file) => ({ path: file.path, chars: file.content.length }));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const projectContextStart = systemPrompt.indexOf("\n\n# Project Context\n");
|
|
365
|
+
if (projectContextStart < 0) return [];
|
|
366
|
+
|
|
367
|
+
const skillsStart = systemPrompt.indexOf("\n<available_skills>", projectContextStart);
|
|
368
|
+
const dateStart = systemPrompt.indexOf("\nCurrent date:", projectContextStart);
|
|
369
|
+
const endCandidates = [skillsStart, dateStart].filter((index) => index > projectContextStart);
|
|
370
|
+
const projectContextEnd = endCandidates.length > 0 ? Math.min(...endCandidates) : systemPrompt.length;
|
|
371
|
+
const contextBlock = systemPrompt.slice(projectContextStart, projectContextEnd);
|
|
372
|
+
const contextFiles: ContextFileDetail[] = [];
|
|
373
|
+
|
|
374
|
+
for (const match of contextBlock.matchAll(/^## (.+)$/gm)) {
|
|
375
|
+
const contextPath = match[1]?.trim();
|
|
376
|
+
if (contextPath) contextFiles.push({ path: contextPath });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return contextFiles;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function extractPromptLineValue(systemPrompt: string, label: string): string | undefined {
|
|
383
|
+
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
384
|
+
const match = systemPrompt.match(new RegExp(`^${escapedLabel}:\\s*(.+)$`, "m"));
|
|
385
|
+
return match?.[1]?.trim();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function getToolParameterSummary(parameters: unknown): string {
|
|
389
|
+
const record = (parameters && typeof parameters === "object" ? parameters : {}) as Record<string, unknown>;
|
|
390
|
+
const properties = record.properties && typeof record.properties === "object" ? Object.keys(record.properties as Record<string, unknown>).length : 0;
|
|
391
|
+
const required = Array.isArray(record.required) ? record.required.length : 0;
|
|
392
|
+
if (properties <= 0) return "no params";
|
|
393
|
+
return `${properties} param${properties === 1 ? "" : "s"}${required > 0 ? `, ${required} required` : ""}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function estimateToolSchemaTokens(tool: InitialPromptToolInfo): number {
|
|
397
|
+
return estimatePromptInjectionTokens(
|
|
398
|
+
JSON.stringify({
|
|
399
|
+
name: tool.name,
|
|
400
|
+
description: tool.description ?? "",
|
|
401
|
+
parameters: tool.parameters ?? {},
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function pushDetailSection(lines: string[], title: string, body: string[]): void {
|
|
407
|
+
const cleanBody = body.filter((line) => line.trim().length > 0);
|
|
408
|
+
if (cleanBody.length === 0) return;
|
|
409
|
+
|
|
410
|
+
const ruleLength = 52;
|
|
411
|
+
const heading = `╭─ ${title} ${"─".repeat(Math.max(3, ruleLength - title.length))}`;
|
|
412
|
+
lines.push("", heading, ...cleanBody.map((line) => `│ ${line}`), `╰${"─".repeat(Math.max(3, heading.length - 1))}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function formatInitialPromptDetailedLines(
|
|
416
|
+
promptEstimate: Awaited<ReturnType<typeof estimateInitialPromptFromPiExport>>,
|
|
417
|
+
options: BuildSystemPromptOptions | null,
|
|
418
|
+
): string[] {
|
|
419
|
+
const systemPrompt = promptEstimate.systemPrompt;
|
|
420
|
+
const estimate = promptEstimate.estimate;
|
|
421
|
+
const tools = promptEstimate.tools;
|
|
422
|
+
const toolPromptEntries = extractAvailableToolPromptEntries(systemPrompt);
|
|
423
|
+
const skills = extractPromptSkills(systemPrompt, options);
|
|
424
|
+
const contextFiles = extractPromptContextFiles(systemPrompt, options);
|
|
425
|
+
const currentDate = extractPromptLineValue(systemPrompt, "Current date");
|
|
426
|
+
const cwd = extractPromptLineValue(systemPrompt, "Current working directory");
|
|
427
|
+
const promptGuidelines = options?.promptGuidelines ?? [];
|
|
428
|
+
const sourceLabel = promptEstimate.source === "export-html" ? "export HTML" : "live context fallback";
|
|
429
|
+
const calibration = estimate.calibrationSamples > 0
|
|
430
|
+
? `×${estimate.calibrationMultiplier.toFixed(2)} (${estimate.calibrationSamples} sample${estimate.calibrationSamples === 1 ? "" : "s"})`
|
|
431
|
+
: "none";
|
|
432
|
+
const detailLines = ["Initial prompt details", "━━━━━━━━━━━━━━━━━━━━━━"];
|
|
433
|
+
|
|
434
|
+
pushDetailSection(detailLines, "Snapshot", [
|
|
435
|
+
`• source: ${sourceLabel}`,
|
|
436
|
+
`• system prompt: ${systemPrompt.length.toLocaleString()} chars`,
|
|
437
|
+
`• active tool schemas: ${tools.length}`,
|
|
438
|
+
`• available-tool prompt entries: ${toolPromptEntries.length}`,
|
|
439
|
+
`• skills in prompt: ${skills.length}`,
|
|
440
|
+
`• context files: ${contextFiles.length}`,
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
pushDetailSection(detailLines, "Estimate components", [
|
|
444
|
+
`• prompt text: ~${formatTokens(estimate.promptText)} tok`,
|
|
445
|
+
`• tool schemas: ~${formatTokens(estimate.toolSchemas)} tok`,
|
|
446
|
+
`• provider/request framing: ~${formatTokens(estimate.framing)} tok`,
|
|
447
|
+
`• calibration: ${calibration}`,
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
const metadataParts = [currentDate ? `date: ${currentDate}` : null, cwd ? `cwd: ${cwd}` : null, promptGuidelines.length > 0 ? `extra guidelines: ${promptGuidelines.length}` : null].filter((part): part is string => !!part);
|
|
451
|
+
pushDetailSection(detailLines, "Prompt metadata", metadataParts.map((part) => `• ${part}`));
|
|
452
|
+
|
|
453
|
+
if (tools.length > 0) {
|
|
454
|
+
const toolSummaries = tools
|
|
455
|
+
.map((tool) => ({
|
|
456
|
+
...tool,
|
|
457
|
+
tokens: estimateToolSchemaTokens(tool),
|
|
458
|
+
parametersSummary: getToolParameterSummary(tool.parameters),
|
|
459
|
+
description: shortenText(tool.description, 72),
|
|
460
|
+
}))
|
|
461
|
+
.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
|
|
462
|
+
const shown = toolSummaries.slice(0, 12);
|
|
463
|
+
const remaining = toolSummaries.length - shown.length;
|
|
464
|
+
const toolLines = shown.map((tool, index) => {
|
|
465
|
+
const description = tool.description ? ` · ${tool.description}` : "";
|
|
466
|
+
return `${String(index + 1).padStart(2, "0")}. ${tool.name} — ~${formatTokens(tool.tokens)} tok · ${tool.parametersSummary}${description}`;
|
|
467
|
+
});
|
|
468
|
+
if (remaining > 0) toolLines.push(`… ${remaining} more active schema${remaining === 1 ? "" : "s"}: ${formatCountedNames(toolSummaries.slice(shown.length).map((tool) => tool.name), 16)}`);
|
|
469
|
+
pushDetailSection(detailLines, `Active tool schemas · top ${shown.length} by size`, toolLines);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
pushDetailSection(
|
|
473
|
+
detailLines,
|
|
474
|
+
"Available-tools prompt entries",
|
|
475
|
+
toolPromptEntries.length > 0 ? [`• ${formatCountedNames(toolPromptEntries.map((tool) => tool.name), 24)}`] : [],
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (skills.length > 0) {
|
|
479
|
+
const shown = skills.slice(0, 10);
|
|
480
|
+
const remaining = skills.length - shown.length;
|
|
481
|
+
const skillLines = shown.map((skill) => {
|
|
482
|
+
const description = skill.description ? ` — ${shortenText(skill.description, 96)}` : "";
|
|
483
|
+
return `• ${skill.name}${description}`;
|
|
484
|
+
});
|
|
485
|
+
if (remaining > 0) skillLines.push(`… ${remaining} more skill${remaining === 1 ? "" : "s"}: ${formatCountedNames(skills.slice(shown.length).map((skill) => skill.name), 16)}`);
|
|
486
|
+
pushDetailSection(detailLines, `Skills in prompt · top ${shown.length}`, skillLines);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (contextFiles.length > 0) {
|
|
490
|
+
const shown = contextFiles.slice(0, 8);
|
|
491
|
+
const remaining = contextFiles.length - shown.length;
|
|
492
|
+
const contextLines = shown.map((file) => {
|
|
493
|
+
const chars = typeof file.chars === "number" ? ` · ${file.chars.toLocaleString()} chars` : "";
|
|
494
|
+
return `• ${file.path}${chars}`;
|
|
495
|
+
});
|
|
496
|
+
if (remaining > 0) contextLines.push(`… ${remaining} more context file${remaining === 1 ? "" : "s"}`);
|
|
497
|
+
pushDetailSection(detailLines, `Context files · ${contextFiles.length}`, contextLines);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return detailLines;
|
|
501
|
+
}
|
|
502
|
+
|
|
262
503
|
function stringifyContextValue(value: unknown): string {
|
|
263
504
|
if (value === undefined || value === null) return "";
|
|
264
505
|
if (typeof value === "string") return value;
|
|
@@ -374,6 +615,15 @@ function parseDaysArg(args: string): { mode: "range"; days: number } | { mode: "
|
|
|
374
615
|
return { mode: "range", days: n };
|
|
375
616
|
}
|
|
376
617
|
|
|
618
|
+
function parseStatsPiArg(args: string): { detailed: boolean } | null {
|
|
619
|
+
const tokens = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
620
|
+
if (tokens.length === 0) return { detailed: false };
|
|
621
|
+
if (tokens.length === 1 && ["detailed", "detail", "details", "--detailed"].includes(tokens[0] ?? "")) {
|
|
622
|
+
return { detailed: true };
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
377
627
|
function listSessionFiles(sessionDir: string): string[] {
|
|
378
628
|
try {
|
|
379
629
|
return fs
|
|
@@ -890,11 +1140,24 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
890
1140
|
});
|
|
891
1141
|
|
|
892
1142
|
pi.registerCommand("stats-pi", {
|
|
893
|
-
description: "Show estimated initial prompt input token breakdown.",
|
|
894
|
-
handler: async (
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
1143
|
+
description: "Show export-backed estimated initial prompt input token breakdown. Usage: /stats-pi [detailed]",
|
|
1144
|
+
handler: async (args, ctx) => {
|
|
1145
|
+
const parsed = parseStatsPiArg(args);
|
|
1146
|
+
if (!parsed) {
|
|
1147
|
+
ctx.ui.notify("Usage: /stats-pi [detailed]", "warning");
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const promptEstimate = await estimateInitialPromptFromPiExport(pi, ctx, getPromptCalibration(ctx));
|
|
1152
|
+
const lines = formatPromptInjectionLines(promptEstimate.systemPrompt, latestSystemPromptOptions, promptEstimate.estimate, {
|
|
1153
|
+
source: promptEstimate.source === "export-html" ? "temporary Pi /export HTML session data" : "live context fallback",
|
|
1154
|
+
warning: promptEstimate.warning,
|
|
1155
|
+
});
|
|
1156
|
+
if (parsed.detailed) {
|
|
1157
|
+
lines.push("", ...formatInitialPromptDetailedLines(promptEstimate, latestSystemPromptOptions));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
898
1161
|
},
|
|
899
1162
|
});
|
|
900
1163
|
|
|
@@ -934,7 +1197,7 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
934
1197
|
});
|
|
935
1198
|
|
|
936
1199
|
pi.registerCommand("stats", {
|
|
937
|
-
description: "Show token usage dashboard. Usage: /stats, /stats 30, /stats all. Details: /stats-tokens, /stats-pi, /stats-last, /stats-most-expense, /stats-model-compare, /stats-cost-trend, /stats-cache",
|
|
1200
|
+
description: "Show token usage dashboard. Usage: /stats, /stats 30, /stats all. Details: /stats-tokens, /stats-pi [detailed], /stats-last, /stats-most-expense, /stats-model-compare, /stats-cost-trend, /stats-cache",
|
|
938
1201
|
handler: async (args, ctx) => {
|
|
939
1202
|
const trimmedArgs = args.trim().toLowerCase();
|
|
940
1203
|
if (trimmedArgs === "tokens") {
|
|
@@ -953,7 +1216,7 @@ export default function statsExtension(pi: ExtensionAPI) {
|
|
|
953
1216
|
const sessionLines = formatExpensiveSessionLines(data.records, data.dayKeys).slice(0, 7);
|
|
954
1217
|
const commandLines = [
|
|
955
1218
|
"Detailed commands:",
|
|
956
|
-
"/stats-last · /stats-most-expense · /stats-model-compare · /stats-pi · /stats-cost-trend · /stats-cache · /stats-tokens",
|
|
1219
|
+
"/stats-last · /stats-most-expense · /stats-model-compare · /stats-pi detailed · /stats-cost-trend · /stats-cache · /stats-tokens",
|
|
957
1220
|
];
|
|
958
1221
|
|
|
959
1222
|
ctx.ui.notify(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-extension-stats",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Token and cost usage analytics command for Pi session history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"extension"
|
|
11
11
|
],
|
|
12
12
|
"pi": {
|
|
13
|
+
"image": "https://unpkg.com/@firstpick/pi-extension-stats/images/stats_v0.1.2.png",
|
|
13
14
|
"extensions": [
|
|
14
15
|
"./index.ts"
|
|
15
16
|
]
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"index.ts",
|
|
26
|
+
"images",
|
|
25
27
|
"README.md",
|
|
26
28
|
"LICENSE"
|
|
27
29
|
]
|