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 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()`.
@@ -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
- msgType === "compaction" ||
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. System messages
67
- if (role === "system" || msgType === "system") {
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
- // 4. User messages — track file attachments
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" || block.type === "image_url") {
84
- const p = block.source?.url || block.image_url?.url || "[image]";
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
- // 5. Assistant messages — track tool_use blocks
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
- if (block.type === "tool_use") {
132
- const input = block.input as Record<string, any>;
133
- const p = this.extractPath(block.name, input);
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
- // 6. Everything else
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, input: any): string | null {
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 input.path === "string" ? input.path : null;
226
+ return typeof args.path === "string" ? args.path : null;
192
227
  }
193
228
  if (toolName === "bash") {
194
- const match = input.command?.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
- if (
232
- messages[i].role === "toolResult" &&
233
- messages[i].tool_call_id === toolId
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 (messages[i].role === "assistant") break;
284
+ if (m.role === "assistant") break;
238
285
  }
239
286
  return null;
240
287
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-context-map
3
3
  * Professional Context Profiler for Pi.
4
- * v0.5.1Dynamic context window, dark mode, session-unique reports.
4
+ * v0.6.2Fixed 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 and context window from Pi system
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(messages, currentTurn);
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. The live localhost report will auto-update.",
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
- `${insights.length} insight(s) generated.`;
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.1",
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",