pi-context-map 0.6.1 → 0.6.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/CHANGELOG.md +11 -0
- package/extensions/analyzer.ts +76 -29
- package/extensions/index.ts +39 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed Pi message format**: Now uses `type: "toolCall"` (not `"tool_use"`) and `toolCallId` (not `tool_call_id`) to match Pi's actual `@mariozechner/pi-ai` types.
|
|
6
|
+
- **System prompt detection**: Now accepts `systemPrompt` parameter from Pi's `ctx.getSystemPrompt()`. System slice no longer shows 0%.
|
|
7
|
+
- **Tool results detection**: Changed to `role === "toolResult"` to match Pi's actual message format.
|
|
8
|
+
- **File tracking from tool results**: Now extracts file paths from `toolResult` messages (read/write/edit tool results).
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- **Message persistence**: Messages are saved via `appendEntry` on compaction to survive session reloads.
|
|
12
|
+
- **Enhanced diagnostics**: `/context-map` command now shows message count, system tokens, and tool tokens in the notification.
|
|
13
|
+
|
|
3
14
|
## [0.6.1] - 2026-06-15
|
|
4
15
|
### Bug Fixes
|
|
5
16
|
- **Fixed libuv assertion on Windows**: Removed `process.on('exit')` handler and `process.exit(0)` calls that left server handles open. Server now closes synchronously via `closeAllConnections()`.
|
package/extensions/analyzer.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* ContextAnalyzer
|
|
3
3
|
* Parses Pi session messages to identify the active working set of files,
|
|
4
4
|
* their token weights, and their temporal status.
|
|
5
|
+
*
|
|
6
|
+
* Pi message format (from @mariozechner/pi-ai):
|
|
7
|
+
* UserMessage: { role: "user", content: string | (TextContent | ImageContent)[] }
|
|
8
|
+
* AssistantMessage: { role: "assistant", content: (TextContent | ThinkingContent | ToolCall)[] }
|
|
9
|
+
* ToolResultMessage: { role: "toolResult", toolCallId, toolName, content: (TextContent | ImageContent)[] }
|
|
10
|
+
*
|
|
11
|
+
* ToolCall: { type: "toolCall", id, name, arguments }
|
|
12
|
+
* ToolCall.id maps to ToolResultMessage.toolCallId
|
|
5
13
|
*/
|
|
6
14
|
import { TokenCounter } from "./token-counter";
|
|
7
15
|
|
|
@@ -37,6 +45,7 @@ export class ContextAnalyzer {
|
|
|
37
45
|
public analyzeByType(
|
|
38
46
|
messages: any[],
|
|
39
47
|
currentTurn: number,
|
|
48
|
+
systemPrompt?: string,
|
|
40
49
|
): ContextComposition {
|
|
41
50
|
const fileRegistry = new Map<string, FileContext>();
|
|
42
51
|
|
|
@@ -46,16 +55,20 @@ export class ContextAnalyzer {
|
|
|
46
55
|
let fileTokens = 0;
|
|
47
56
|
let summaryTokens = 0;
|
|
48
57
|
|
|
58
|
+
// Count system prompt tokens if provided
|
|
59
|
+
if (systemPrompt && systemPrompt.length > 0) {
|
|
60
|
+
systemTokens += TokenCounter.count(systemPrompt);
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
for (let index = 0; index < messages.length; index++) {
|
|
50
64
|
const msg = messages[index];
|
|
51
65
|
const turn = index + 1;
|
|
52
66
|
const role = msg.role || "";
|
|
53
|
-
const msgType = msg.type || "";
|
|
54
67
|
|
|
55
|
-
// 1. Compaction summaries
|
|
68
|
+
// 1. Compaction summaries (Pi compaction entries)
|
|
56
69
|
if (
|
|
57
70
|
role === "compaction" ||
|
|
58
|
-
|
|
71
|
+
msg.type === "compaction" ||
|
|
59
72
|
msg.customType === "compaction" ||
|
|
60
73
|
msg.compactionEntry
|
|
61
74
|
) {
|
|
@@ -63,25 +76,46 @@ export class ContextAnalyzer {
|
|
|
63
76
|
continue;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
// 2.
|
|
67
|
-
if (role === "
|
|
68
|
-
systemTokens += TokenCounter.countMessage(msg);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 3. Tool results (Pi uses "toolResult")
|
|
73
|
-
if (role === "toolResult" || role === "tool") {
|
|
79
|
+
// 2. Tool results (Pi uses role="toolResult")
|
|
80
|
+
if (role === "toolResult") {
|
|
74
81
|
toolTokens += TokenCounter.countMessage(msg);
|
|
82
|
+
// Track file content from tool results
|
|
83
|
+
const toolName = msg.toolName || "";
|
|
84
|
+
if (
|
|
85
|
+
toolName === "read" ||
|
|
86
|
+
toolName === "write" ||
|
|
87
|
+
toolName === "edit"
|
|
88
|
+
) {
|
|
89
|
+
const content = msg.content;
|
|
90
|
+
const path = this.extractPathFromToolResult(content);
|
|
91
|
+
if (path) {
|
|
92
|
+
const w = TokenCounter.countMessage(msg);
|
|
93
|
+
fileTokens += w;
|
|
94
|
+
if (!fileRegistry.has(path)) {
|
|
95
|
+
fileRegistry.set(path, {
|
|
96
|
+
path,
|
|
97
|
+
weight: w,
|
|
98
|
+
lastOp: {
|
|
99
|
+
type: this.getOpType(toolName),
|
|
100
|
+
turn,
|
|
101
|
+
timestamp: msg.timestamp || Date.now(),
|
|
102
|
+
},
|
|
103
|
+
status: this.calculateStatus(turn, currentTurn),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
75
108
|
continue;
|
|
76
109
|
}
|
|
77
110
|
|
|
78
|
-
//
|
|
111
|
+
// 3. User messages
|
|
79
112
|
if (role === "user") {
|
|
80
113
|
historyTokens += TokenCounter.countMessage(msg);
|
|
114
|
+
// Track file attachments (images, file paths in text)
|
|
81
115
|
if (Array.isArray(msg.content)) {
|
|
82
116
|
for (const block of msg.content) {
|
|
83
|
-
if (block.type === "image"
|
|
84
|
-
const p =
|
|
117
|
+
if (block.type === "image") {
|
|
118
|
+
const p = "[image]";
|
|
85
119
|
const w = TokenCounter.count(JSON.stringify(block));
|
|
86
120
|
fileTokens += w;
|
|
87
121
|
if (!fileRegistry.has(p)) {
|
|
@@ -123,19 +157,19 @@ export class ContextAnalyzer {
|
|
|
123
157
|
continue;
|
|
124
158
|
}
|
|
125
159
|
|
|
126
|
-
//
|
|
160
|
+
// 4. Assistant messages — track toolCall blocks
|
|
127
161
|
if (role === "assistant") {
|
|
128
162
|
historyTokens += TokenCounter.countMessage(msg);
|
|
129
163
|
if (Array.isArray(msg.content)) {
|
|
130
164
|
for (const block of msg.content) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const p = this.extractPath(block.name,
|
|
165
|
+
// Pi uses type="toolCall" with id, name, arguments
|
|
166
|
+
if (block.type === "toolCall") {
|
|
167
|
+
const p = this.extractPath(block.name, block.arguments);
|
|
134
168
|
if (p) {
|
|
135
169
|
const opType = this.getOpType(block.name);
|
|
136
170
|
const result = this.findToolResult(messages, index, block.id);
|
|
137
171
|
const content = result?.content || "";
|
|
138
|
-
const w = TokenCounter.count(String(content));
|
|
172
|
+
const w = TokenCounter.count(String(JSON.stringify(content)));
|
|
139
173
|
fileTokens += w;
|
|
140
174
|
fileRegistry.set(p, {
|
|
141
175
|
path: p,
|
|
@@ -154,7 +188,7 @@ export class ContextAnalyzer {
|
|
|
154
188
|
continue;
|
|
155
189
|
}
|
|
156
190
|
|
|
157
|
-
//
|
|
191
|
+
// 5. Everything else
|
|
158
192
|
historyTokens += TokenCounter.countMessage(msg);
|
|
159
193
|
}
|
|
160
194
|
|
|
@@ -186,12 +220,13 @@ export class ContextAnalyzer {
|
|
|
186
220
|
return this.analyzeByType(messages, currentTurn);
|
|
187
221
|
}
|
|
188
222
|
|
|
189
|
-
private extractPath(toolName: string,
|
|
223
|
+
private extractPath(toolName: string, args: any): string | null {
|
|
224
|
+
if (!args || typeof args !== "object") return null;
|
|
190
225
|
if (toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
191
|
-
return typeof
|
|
226
|
+
return typeof args.path === "string" ? args.path : null;
|
|
192
227
|
}
|
|
193
228
|
if (toolName === "bash") {
|
|
194
|
-
const match =
|
|
229
|
+
const match = args.command?.match(
|
|
195
230
|
/(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
|
|
196
231
|
);
|
|
197
232
|
return match ? match[1] : null;
|
|
@@ -199,6 +234,19 @@ export class ContextAnalyzer {
|
|
|
199
234
|
return null;
|
|
200
235
|
}
|
|
201
236
|
|
|
237
|
+
private extractPathFromToolResult(content: any): string | null {
|
|
238
|
+
if (typeof content === "string") return null;
|
|
239
|
+
if (Array.isArray(content)) {
|
|
240
|
+
for (const block of content) {
|
|
241
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
242
|
+
const match = block.text.match(/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/);
|
|
243
|
+
if (match) return match[0];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
private getOpType(toolName: string): FileOp["type"] {
|
|
203
251
|
switch (toolName) {
|
|
204
252
|
case "write":
|
|
@@ -228,13 +276,12 @@ export class ContextAnalyzer {
|
|
|
228
276
|
toolId: string,
|
|
229
277
|
): any {
|
|
230
278
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return messages[i];
|
|
279
|
+
const m = messages[i];
|
|
280
|
+
// Pi uses role="toolResult" and toolCallId (not tool_call_id)
|
|
281
|
+
if (m.role === "toolResult" && m.toolCallId === toolId) {
|
|
282
|
+
return m;
|
|
236
283
|
}
|
|
237
|
-
if (
|
|
284
|
+
if (m.role === "assistant") break;
|
|
238
285
|
}
|
|
239
286
|
return null;
|
|
240
287
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-context-map
|
|
3
3
|
* Professional Context Profiler for Pi.
|
|
4
|
-
* v0.
|
|
4
|
+
* v0.6.2 — Fixed Pi message format (toolCall), system prompt detection, message persistence.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
@@ -37,9 +37,10 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
37
37
|
let sessionMessages: AgentMessage[] = [];
|
|
38
38
|
let currentTurn = 0;
|
|
39
39
|
let contextWindow = 128_000;
|
|
40
|
+
let systemPrompt = "";
|
|
40
41
|
let currentReportPath = makeReportPath();
|
|
41
42
|
|
|
42
|
-
// Capture messages
|
|
43
|
+
// Capture messages, context window, and system prompt from Pi system
|
|
43
44
|
pi.on("context", (event: any, ctx: any) => {
|
|
44
45
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
45
46
|
sessionMessages = event.messages;
|
|
@@ -52,6 +53,15 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
52
53
|
} catch {
|
|
53
54
|
// Keep fallback
|
|
54
55
|
}
|
|
56
|
+
// Get system prompt from Pi
|
|
57
|
+
try {
|
|
58
|
+
const sp = ctx?.getSystemPrompt?.();
|
|
59
|
+
if (sp && typeof sp === "string") {
|
|
60
|
+
systemPrompt = sp;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Keep empty
|
|
64
|
+
}
|
|
55
65
|
});
|
|
56
66
|
|
|
57
67
|
pi.on("turn_start", () => {
|
|
@@ -63,13 +73,32 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
63
73
|
currentReportPath = makeReportPath();
|
|
64
74
|
});
|
|
65
75
|
|
|
76
|
+
// Persist messages on compaction so they survive reload
|
|
77
|
+
pi.on("session_compact", (event: any) => {
|
|
78
|
+
if (event?.compactionEntry) {
|
|
79
|
+
try {
|
|
80
|
+
pi.appendEntry("context-map-snapshot", {
|
|
81
|
+
messages: sessionMessages.slice(-50), // Keep last 50 messages
|
|
82
|
+
turn: currentTurn,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore persistence errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
66
91
|
async function runAnalysis(): Promise<{
|
|
67
92
|
composition: ReturnType<typeof analyzer.analyzeByType>;
|
|
68
93
|
insights: ReturnType<typeof InsightEngine.generate>;
|
|
69
94
|
reportPath: string;
|
|
70
95
|
}> {
|
|
71
96
|
const messages = sessionMessages.length > 0 ? sessionMessages : [];
|
|
72
|
-
const composition = analyzer.analyzeByType(
|
|
97
|
+
const composition = analyzer.analyzeByType(
|
|
98
|
+
messages,
|
|
99
|
+
currentTurn,
|
|
100
|
+
systemPrompt,
|
|
101
|
+
);
|
|
73
102
|
const insights = InsightEngine.generate(composition);
|
|
74
103
|
const html = ReportGenerator.generateHTML(
|
|
75
104
|
composition,
|
|
@@ -108,7 +137,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
108
137
|
|
|
109
138
|
ctx.ui.notify("Analyzing session context...", "info");
|
|
110
139
|
try {
|
|
111
|
-
const { insights, reportPath } = await runAnalysis();
|
|
140
|
+
const { composition, insights, reportPath } = await runAnalysis();
|
|
112
141
|
const criticalCount = insights.filter(
|
|
113
142
|
(i) => i.severity === "critical",
|
|
114
143
|
).length;
|
|
@@ -120,6 +149,9 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
120
149
|
if (serverUrl) {
|
|
121
150
|
details += ` | Live: ${serverUrl}`;
|
|
122
151
|
}
|
|
152
|
+
details += ` | Messages: ${sessionMessages.length}`;
|
|
153
|
+
details += ` | System: ${composition.system.tokens}t (${composition.system.percent}%)`;
|
|
154
|
+
details += ` | Tools: ${composition.tools.tokens}t (${composition.tools.percent}%)`;
|
|
123
155
|
ctx.ui.notify(
|
|
124
156
|
`${summary} ${details}`,
|
|
125
157
|
criticalCount > 0 ? "warning" : "success",
|
|
@@ -137,7 +169,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
137
169
|
name: "context-map",
|
|
138
170
|
label: "Context Map",
|
|
139
171
|
description:
|
|
140
|
-
"Analyze the current session context composition and return actionable insights.
|
|
172
|
+
"Analyze the current session context composition and return actionable insights.",
|
|
141
173
|
parameters: {
|
|
142
174
|
type: "object",
|
|
143
175
|
properties: {},
|
|
@@ -160,7 +192,8 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
160
192
|
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
161
193
|
`Summaries ${composition.summaries.percent}%. ` +
|
|
162
194
|
`Usage: ${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window. ` +
|
|
163
|
-
|
|
195
|
+
`Messages: ${sessionMessages.length}. ` +
|
|
196
|
+
`${insights.length} insight(s).`;
|
|
164
197
|
return {
|
|
165
198
|
type: "text" as const,
|
|
166
199
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Professional context profiler for Pi that visualizes the session context window, token distribution, and integrates with Nexus packages for actionable insights.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|