pi-context-map 0.6.0 → 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 +16 -0
- package/extensions/analyzer.ts +78 -37
- package/extensions/index.ts +40 -12
- package/extensions/live-server.ts +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.6.1] - 2026-06-15
|
|
15
|
+
### Bug Fixes
|
|
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()`.
|
|
17
|
+
- **Synchronous stop()**: `isRunning` returns `false` immediately after `stop()` instead of after async callback.
|
|
18
|
+
|
|
3
19
|
## [0.6.0] - 2026-06-15
|
|
4
20
|
### Bug Fixes
|
|
5
21
|
- **Fixed composition analysis**: Changed `role === "tool"` to `role === "toolResult"` to match Pi's actual message format. Tools and files now show correct percentages instead of 0%.
|
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,26 +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 =
|
|
85
|
-
block.source?.url || block.image_url?.url || "[image]";
|
|
117
|
+
if (block.type === "image") {
|
|
118
|
+
const p = "[image]";
|
|
86
119
|
const w = TokenCounter.count(JSON.stringify(block));
|
|
87
120
|
fileTokens += w;
|
|
88
121
|
if (!fileRegistry.has(p)) {
|
|
@@ -124,23 +157,19 @@ export class ContextAnalyzer {
|
|
|
124
157
|
continue;
|
|
125
158
|
}
|
|
126
159
|
|
|
127
|
-
//
|
|
160
|
+
// 4. Assistant messages — track toolCall blocks
|
|
128
161
|
if (role === "assistant") {
|
|
129
162
|
historyTokens += TokenCounter.countMessage(msg);
|
|
130
163
|
if (Array.isArray(msg.content)) {
|
|
131
164
|
for (const block of msg.content) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
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);
|
|
135
168
|
if (p) {
|
|
136
169
|
const opType = this.getOpType(block.name);
|
|
137
|
-
const result = this.findToolResult(
|
|
138
|
-
messages,
|
|
139
|
-
index,
|
|
140
|
-
block.id,
|
|
141
|
-
);
|
|
170
|
+
const result = this.findToolResult(messages, index, block.id);
|
|
142
171
|
const content = result?.content || "";
|
|
143
|
-
const w = TokenCounter.count(String(content));
|
|
172
|
+
const w = TokenCounter.count(String(JSON.stringify(content)));
|
|
144
173
|
fileTokens += w;
|
|
145
174
|
fileRegistry.set(p, {
|
|
146
175
|
path: p,
|
|
@@ -159,7 +188,7 @@ export class ContextAnalyzer {
|
|
|
159
188
|
continue;
|
|
160
189
|
}
|
|
161
190
|
|
|
162
|
-
//
|
|
191
|
+
// 5. Everything else
|
|
163
192
|
historyTokens += TokenCounter.countMessage(msg);
|
|
164
193
|
}
|
|
165
194
|
|
|
@@ -168,8 +197,7 @@ export class ContextAnalyzer {
|
|
|
168
197
|
|
|
169
198
|
const mk = (tokens: number): ContextSlice => ({
|
|
170
199
|
tokens: Math.ceil(tokens),
|
|
171
|
-
percent:
|
|
172
|
-
totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
200
|
+
percent: totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
173
201
|
});
|
|
174
202
|
|
|
175
203
|
const files_detail = Array.from(fileRegistry.values())
|
|
@@ -192,12 +220,13 @@ export class ContextAnalyzer {
|
|
|
192
220
|
return this.analyzeByType(messages, currentTurn);
|
|
193
221
|
}
|
|
194
222
|
|
|
195
|
-
private extractPath(toolName: string,
|
|
223
|
+
private extractPath(toolName: string, args: any): string | null {
|
|
224
|
+
if (!args || typeof args !== "object") return null;
|
|
196
225
|
if (toolName === "read" || toolName === "write" || toolName === "edit") {
|
|
197
|
-
return typeof
|
|
226
|
+
return typeof args.path === "string" ? args.path : null;
|
|
198
227
|
}
|
|
199
228
|
if (toolName === "bash") {
|
|
200
|
-
const match =
|
|
229
|
+
const match = args.command?.match(
|
|
201
230
|
/(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
|
|
202
231
|
);
|
|
203
232
|
return match ? match[1] : null;
|
|
@@ -205,6 +234,19 @@ export class ContextAnalyzer {
|
|
|
205
234
|
return null;
|
|
206
235
|
}
|
|
207
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
|
+
|
|
208
250
|
private getOpType(toolName: string): FileOp["type"] {
|
|
209
251
|
switch (toolName) {
|
|
210
252
|
case "write":
|
|
@@ -234,13 +276,12 @@ export class ContextAnalyzer {
|
|
|
234
276
|
toolId: string,
|
|
235
277
|
): any {
|
|
236
278
|
for (let i = toolTurnIndex + 1; i < messages.length; i++) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
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;
|
|
242
283
|
}
|
|
243
|
-
if (
|
|
284
|
+
if (m.role === "assistant") break;
|
|
244
285
|
}
|
|
245
286
|
return null;
|
|
246
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 {
|
|
@@ -25,9 +25,7 @@ function makeReportPath(sessionName?: string): string {
|
|
|
25
25
|
const now = new Date();
|
|
26
26
|
const date = now.toISOString().split("T")[0];
|
|
27
27
|
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
|
|
28
|
-
const safe = (sessionName || "session")
|
|
29
|
-
.replace(/[^\w.-]/g, "_")
|
|
30
|
-
.slice(0, 40);
|
|
28
|
+
const safe = (sessionName || "session").replace(/[^\w.-]/g, "_").slice(0, 40);
|
|
31
29
|
const filename = `${date}_${time}_${safe}.html`;
|
|
32
30
|
return path.join(dir, filename);
|
|
33
31
|
}
|
|
@@ -39,9 +37,10 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
39
37
|
let sessionMessages: AgentMessage[] = [];
|
|
40
38
|
let currentTurn = 0;
|
|
41
39
|
let contextWindow = 128_000;
|
|
40
|
+
let systemPrompt = "";
|
|
42
41
|
let currentReportPath = makeReportPath();
|
|
43
42
|
|
|
44
|
-
// Capture messages
|
|
43
|
+
// Capture messages, context window, and system prompt from Pi system
|
|
45
44
|
pi.on("context", (event: any, ctx: any) => {
|
|
46
45
|
if (event?.messages && Array.isArray(event.messages)) {
|
|
47
46
|
sessionMessages = event.messages;
|
|
@@ -54,6 +53,15 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
54
53
|
} catch {
|
|
55
54
|
// Keep fallback
|
|
56
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
|
+
}
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
pi.on("turn_start", () => {
|
|
@@ -65,13 +73,32 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
65
73
|
currentReportPath = makeReportPath();
|
|
66
74
|
});
|
|
67
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
|
+
|
|
68
91
|
async function runAnalysis(): Promise<{
|
|
69
92
|
composition: ReturnType<typeof analyzer.analyzeByType>;
|
|
70
93
|
insights: ReturnType<typeof InsightEngine.generate>;
|
|
71
94
|
reportPath: string;
|
|
72
95
|
}> {
|
|
73
96
|
const messages = sessionMessages.length > 0 ? sessionMessages : [];
|
|
74
|
-
const composition = analyzer.analyzeByType(
|
|
97
|
+
const composition = analyzer.analyzeByType(
|
|
98
|
+
messages,
|
|
99
|
+
currentTurn,
|
|
100
|
+
systemPrompt,
|
|
101
|
+
);
|
|
75
102
|
const insights = InsightEngine.generate(composition);
|
|
76
103
|
const html = ReportGenerator.generateHTML(
|
|
77
104
|
composition,
|
|
@@ -110,7 +137,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
110
137
|
|
|
111
138
|
ctx.ui.notify("Analyzing session context...", "info");
|
|
112
139
|
try {
|
|
113
|
-
const { insights, reportPath } = await runAnalysis();
|
|
140
|
+
const { composition, insights, reportPath } = await runAnalysis();
|
|
114
141
|
const criticalCount = insights.filter(
|
|
115
142
|
(i) => i.severity === "critical",
|
|
116
143
|
).length;
|
|
@@ -122,6 +149,9 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
122
149
|
if (serverUrl) {
|
|
123
150
|
details += ` | Live: ${serverUrl}`;
|
|
124
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}%)`;
|
|
125
155
|
ctx.ui.notify(
|
|
126
156
|
`${summary} ${details}`,
|
|
127
157
|
criticalCount > 0 ? "warning" : "success",
|
|
@@ -139,7 +169,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
139
169
|
name: "context-map",
|
|
140
170
|
label: "Context Map",
|
|
141
171
|
description:
|
|
142
|
-
"Analyze the current session context composition and return actionable insights.
|
|
172
|
+
"Analyze the current session context composition and return actionable insights.",
|
|
143
173
|
parameters: {
|
|
144
174
|
type: "object",
|
|
145
175
|
properties: {},
|
|
@@ -162,7 +192,8 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
162
192
|
`History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
|
|
163
193
|
`Summaries ${composition.summaries.percent}%. ` +
|
|
164
194
|
`Usage: ${usagePercent}% of ${(contextWindow / 1000).toFixed(0)}k window. ` +
|
|
165
|
-
|
|
195
|
+
`Messages: ${sessionMessages.length}. ` +
|
|
196
|
+
`${insights.length} insight(s).`;
|
|
166
197
|
return {
|
|
167
198
|
type: "text" as const,
|
|
168
199
|
content: [
|
|
@@ -211,13 +242,10 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
211
242
|
liveServer.stop();
|
|
212
243
|
});
|
|
213
244
|
|
|
214
|
-
process.on("exit", () => liveServer.stop());
|
|
215
245
|
process.on("SIGINT", () => {
|
|
216
246
|
liveServer.stop();
|
|
217
|
-
process.exit(0);
|
|
218
247
|
});
|
|
219
248
|
process.on("SIGTERM", () => {
|
|
220
249
|
liveServer.stop();
|
|
221
|
-
process.exit(0);
|
|
222
250
|
});
|
|
223
251
|
}
|
|
@@ -94,18 +94,19 @@ export class LiveReportServer {
|
|
|
94
94
|
for (const client of this.clients) {
|
|
95
95
|
try {
|
|
96
96
|
client.end();
|
|
97
|
-
} catch
|
|
98
|
-
// Ignore
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
this.clients.clear();
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
this.server.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
// Force-close all connections synchronously (Node 18.2+)
|
|
104
|
+
if (typeof this.server.closeAllConnections === "function") {
|
|
105
|
+
this.server.closeAllConnections();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Close server and reset state synchronously
|
|
109
|
+
this.server.close();
|
|
109
110
|
this.server = null;
|
|
110
111
|
this.port = 0;
|
|
111
112
|
}
|
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",
|