pi-context-map 0.4.0 → 0.4.3

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.
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * pi-context-map
3
3
  * Professional Context Profiler for Pi.
4
- * v0.4.0 - Adds live localhost server with auto-updates.
4
+ * v0.4.1 Fixes session.messages crash, tool registration signature, adds tests.
5
5
  */
6
6
 
7
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type {
8
+ ExtensionAPI,
9
+ ExtensionCommandContext,
10
+ AgentMessage,
11
+ } from "pi-coding-agent";
8
12
  import { ContextAnalyzer } from "./analyzer";
9
13
  import { ReportGenerator } from "./generator";
10
14
  import { InsightEngine } from "./insights";
@@ -12,30 +16,57 @@ import { LiveReportServer } from "./live-server";
12
16
  import * as path from "node:path";
13
17
  import * as os from "node:os";
14
18
 
15
- const REPORT_PATH = path.join(os.homedir(), ".pi", "context-map", "report.html");
19
+ const REPORT_PATH = path.join(
20
+ os.homedir(),
21
+ ".pi",
22
+ "context-map",
23
+ "report.html",
24
+ );
16
25
 
17
- export default async function piContextMap(pi: ExtensionAPI) {
26
+ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
18
27
  const analyzer = new ContextAnalyzer();
19
28
  const liveServer = new LiveReportServer();
20
29
 
21
- async function runAnalysis() {
22
- const messages = (pi as any).session?.messages || [];
23
- const currentTurn = messages.length;
30
+ // Accumulate messages from events — this is the correct way to access
31
+ // session messages in Pi. (pi as any).session?.messages does NOT exist.
32
+ let sessionMessages: AgentMessage[] = [];
33
+ let currentTurn = 0;
34
+
35
+ // Capture messages before each LLM call
36
+ pi.on("context", (event: any) => {
37
+ if (event?.messages && Array.isArray(event.messages)) {
38
+ sessionMessages = event.messages;
39
+ }
40
+ });
41
+
42
+ // Track turns
43
+ pi.on("turn_start", () => {
44
+ currentTurn++;
45
+ });
46
+
47
+ async function runAnalysis(): Promise<{
48
+ composition: ReturnType<typeof analyzer.analyzeByType>;
49
+ insights: ReturnType<typeof InsightEngine.generate>;
50
+ reportPath: string;
51
+ }> {
52
+ const messages = sessionMessages.length > 0 ? sessionMessages : [];
24
53
  const composition = analyzer.analyzeByType(messages, currentTurn);
25
54
  const insights = InsightEngine.generate(composition);
26
55
  const html = ReportGenerator.generateHTML(composition, insights);
27
56
 
28
- // Write to disk (for offline access / persistence)
57
+ // Write to disk
29
58
  try {
30
59
  const fs = await import("node:fs");
31
60
  const dir = path.dirname(REPORT_PATH);
32
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
33
64
  fs.writeFileSync(REPORT_PATH, html, "utf8");
34
65
  } catch (err: any) {
35
- console.error(`[pi-context-map] Failed to write report to disk: ${err.message}`);
66
+ console.error(`[pi-context-map] Failed to write report: ${err.message}`);
36
67
  }
37
68
 
38
- // Push to live server (if running) so the browser updates instantly
69
+ // Push to live server if running
39
70
  if (liveServer.isRunning) {
40
71
  liveServer.update(html, REPORT_PATH);
41
72
  }
@@ -43,14 +74,15 @@ export default async function piContextMap(pi: ExtensionAPI) {
43
74
  return { composition, insights, reportPath: REPORT_PATH };
44
75
  }
45
76
 
46
- // Start the live server on load
77
+ // Start live server
47
78
  const serverUrl = await liveServer.start();
48
79
 
80
+ // Register /context-map command
49
81
  pi.registerCommand("context-map", {
50
- description: "Generate a visual context map with actionable insights. Use 'stop' to terminate the live server.",
51
- handler: async (args: any, ctx: any) => {
52
- // Handle subcommand: /context-map stop
53
- if (typeof args === "string" && args.trim().toLowerCase() === "stop") {
82
+ description:
83
+ "Generate a visual context map with actionable insights. Use 'stop' to terminate the live server.",
84
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
85
+ if (args.trim().toLowerCase() === "stop") {
54
86
  liveServer.stop();
55
87
  ctx.ui.notify("Live server stopped.", "info");
56
88
  return;
@@ -59,66 +91,85 @@ export default async function piContextMap(pi: ExtensionAPI) {
59
91
  ctx.ui.notify("Analyzing session context...", "info");
60
92
  try {
61
93
  const { insights, reportPath } = await runAnalysis();
62
- const criticalCount = insights.filter((i: any) => i.severity === "critical").length;
63
- const summary = criticalCount > 0
64
- ? `Context map generated. ${criticalCount} critical insight(s) found.`
65
- : `Context map generated successfully.`;
66
-
94
+ const criticalCount = insights.filter(
95
+ (i) => i.severity === "critical",
96
+ ).length;
97
+ const summary =
98
+ criticalCount > 0
99
+ ? `Context map generated. ${criticalCount} critical insight(s) found.`
100
+ : "Context map generated successfully.";
67
101
  let details = `File: ${reportPath}`;
68
102
  if (serverUrl) {
69
- details += ` Live: ${serverUrl}`;
103
+ details += ` | Live: ${serverUrl}`;
70
104
  }
71
- ctx.ui.notify(`${summary} ${details}`, criticalCount > 0 ? "warning" : "success");
105
+ ctx.ui.notify(
106
+ `${summary} ${details}`,
107
+ criticalCount > 0 ? "warning" : "success",
108
+ );
72
109
  } catch (error: any) {
73
- ctx.ui.notify(`Failed to generate context map: ${error.message}`, "error");
110
+ ctx.ui.notify(
111
+ `Failed to generate context map: ${error.message}`,
112
+ "error",
113
+ );
74
114
  }
75
115
  },
76
116
  });
77
117
 
118
+ // Register the tool for agent use
78
119
  pi.registerTool({
79
120
  name: "context-map",
121
+ label: "Context Map",
80
122
  description:
81
123
  "Analyze the current session context composition and return actionable insights. The live localhost report will auto-update.",
82
124
  parameters: {
83
125
  type: "object",
84
126
  properties: {},
85
127
  },
86
- handler: async (_ctx: any, _args: any) => {
128
+ async execute(
129
+ _params: any,
130
+ _signal: AbortSignal | undefined,
131
+ _onUpdate: ((u: any) => void) | undefined,
132
+ _ctx: any,
133
+ ) {
87
134
  try {
88
135
  const { composition, insights } = await runAnalysis();
136
+ const usagePercent =
137
+ composition.total.tokens > 0
138
+ ? Math.round((composition.total.tokens / 128_000) * 100)
139
+ : 0;
89
140
  const summary =
90
141
  `Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
91
142
  `System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
92
143
  `History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
93
144
  `Summaries ${composition.summaries.percent}%. ` +
145
+ `Usage: ${usagePercent}% of typical 128k window. ` +
94
146
  `${insights.length} insight(s) generated.`;
95
147
  return {
96
- summary,
97
- composition: {
98
- system: composition.system.tokens,
99
- tools: composition.tools.tokens,
100
- history: composition.history.tokens,
101
- files: composition.files.tokens,
102
- summaries: composition.summaries.tokens,
103
- total: composition.total.tokens,
104
- },
105
- insights: insights.map((i: any) => ({
106
- severity: i.severity,
107
- title: i.title,
108
- message: i.message,
109
- command: i.command,
110
- })),
111
- liveUrl: serverUrl,
112
- reportPath: REPORT_PATH,
148
+ type: "text" as const,
149
+ content: [
150
+ summary,
151
+ "",
152
+ ...insights.map(
153
+ (i) => `[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
154
+ ),
155
+ serverUrl ? `Live report: ${serverUrl}` : "",
156
+ ]
157
+ .filter(Boolean)
158
+ .join("\n"),
113
159
  };
114
160
  } catch (error: any) {
115
- return { error: error.message };
161
+ return {
162
+ type: "text" as const,
163
+ content: `Error: ${error.message}`,
164
+ isError: true,
165
+ };
116
166
  }
117
167
  },
118
168
  });
119
169
 
120
- pi.on("session_before_compact", (event: any, ctx: any) => {
121
- const tokens = event?.preparation?.tokensBefore;
170
+ // Auto-warning on high context before compaction
171
+ pi.on("session_before_compact", (_event: any, ctx: any) => {
172
+ const tokens = _event?.preparation?.tokensBefore;
122
173
  if (tokens && tokens > 100_000) {
123
174
  ctx.ui.notify(
124
175
  `High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
@@ -127,9 +178,9 @@ export default async function piContextMap(pi: ExtensionAPI) {
127
178
  }
128
179
  });
129
180
 
130
- // Auto-refresh: re-run analysis after each assistant message so the live view stays current
131
- pi.on("message_end", async (event: any) => {
132
- if (event?.message?.role === "assistant" && liveServer.isRunning) {
181
+ // Auto-refresh after each assistant message if server is running
182
+ pi.on("message_end", async (_event: any) => {
183
+ if (_event?.message?.role === "assistant" && liveServer.isRunning) {
133
184
  try {
134
185
  await runAnalysis();
135
186
  } catch {
@@ -138,14 +189,19 @@ export default async function piContextMap(pi: ExtensionAPI) {
138
189
  }
139
190
  });
140
191
 
141
- // Graceful shutdown: stop the live server when the session ends
192
+ // Graceful shutdown
142
193
  pi.on("session_shutdown", () => {
143
194
  liveServer.stop();
144
195
  });
145
196
 
146
- // Log the live URL once on startup
147
- if (serverUrl) {
148
- console.log(`[pi-context-map] Live server running at ${serverUrl}`);
149
- console.log(`[pi-context-map] Run /context-map to generate a report, or /context-map stop to terminate.`);
150
- }
197
+ // Kill server when process exits
198
+ process.on("exit", () => liveServer.stop());
199
+ process.on("SIGINT", () => {
200
+ liveServer.stop();
201
+ process.exit(0);
202
+ });
203
+ process.on("SIGTERM", () => {
204
+ liveServer.stop();
205
+ process.exit(0);
206
+ });
151
207
  }
@@ -17,7 +17,12 @@ import * as os from "node:os";
17
17
  import * as crypto from "node:crypto";
18
18
  import type { AddressInfo } from "node:net";
19
19
 
20
- const DEFAULT_REPORT_PATH = path.join(os.homedir(), ".pi", "context-map", "report.html");
20
+ const DEFAULT_REPORT_PATH = path.join(
21
+ os.homedir(),
22
+ ".pi",
23
+ "context-map",
24
+ "report.html",
25
+ );
21
26
 
22
27
  /**
23
28
  * Allowed origins for SSE connections. Only localhost variants are allowed.
@@ -52,7 +57,9 @@ export class LiveReportServer {
52
57
 
53
58
  return new Promise((resolve) => {
54
59
  try {
55
- this.server = http.createServer((req, res) => this.handleRequest(req, res));
60
+ this.server = http.createServer((req, res) =>
61
+ this.handleRequest(req, res),
62
+ );
56
63
  this.server.on("error", (err) => {
57
64
  console.error(`[pi-context-map] Server error: ${err.message}`);
58
65
  this.stop();
@@ -62,14 +69,15 @@ export class LiveReportServer {
62
69
  const addr = this.server?.address() as AddressInfo | null;
63
70
  if (addr) {
64
71
  this.port = addr.port;
65
- console.log(`[pi-context-map] Live server: ${this.url}`);
66
72
  resolve(this.url);
67
73
  } else {
68
74
  resolve(null);
69
75
  }
70
76
  });
71
77
  } catch (err: any) {
72
- console.error(`[pi-context-map] Failed to start server: ${err.message}`);
78
+ console.error(
79
+ `[pi-context-map] Failed to start server: ${err.message}`,
80
+ );
73
81
  resolve(null);
74
82
  }
75
83
  });
@@ -118,14 +126,18 @@ export class LiveReportServer {
118
126
  }
119
127
  fs.writeFileSync(reportPath, html, "utf8");
120
128
  } catch (err: any) {
121
- console.error(`[pi-context-map] Failed to write report: ${err.message}`);
129
+ console.error(
130
+ `[pi-context-map] Failed to write report: ${err.message}`,
131
+ );
122
132
  }
123
133
  }
124
134
 
125
135
  // Broadcast to all SSE clients
126
136
  for (const client of this.clients) {
127
137
  try {
128
- client.write(`data: ${JSON.stringify({ html, timestamp: Date.now() })}\n\n`);
138
+ client.write(
139
+ `data: ${JSON.stringify({ html, timestamp: Date.now() })}\n\n`,
140
+ );
129
141
  } catch (err) {
130
142
  // Client may have disconnected; remove it
131
143
  this.clients.delete(client);
@@ -151,7 +163,10 @@ export class LiveReportServer {
151
163
  /**
152
164
  * Handle incoming HTTP requests.
153
165
  */
154
- private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
166
+ private handleRequest(
167
+ req: http.IncomingMessage,
168
+ res: http.ServerResponse,
169
+ ): void {
155
170
  if (!req.url) {
156
171
  res.writeHead(400);
157
172
  res.end("Bad request");
@@ -243,7 +258,9 @@ export class LiveReportServer {
243
258
 
244
259
  // Send initial state if we have content
245
260
  if (this.currentHtml) {
246
- res.write(`data: ${JSON.stringify({ html: this.currentHtml, timestamp: Date.now() })}\n\n`);
261
+ res.write(
262
+ `data: ${JSON.stringify({ html: this.currentHtml, timestamp: Date.now() })}\n\n`,
263
+ );
247
264
  } else {
248
265
  res.write(`data: ${JSON.stringify({ waiting: true })}\n\n`);
249
266
  }
@@ -1,3 +1,101 @@
1
- declare module "@earendil-works/pi-coding-agent" {
2
- export type ExtensionAPI = any;
1
+ declare module "pi-coding-agent" {
2
+ export interface ExtensionAPI {
3
+ on(event: string, handler: Function): void;
4
+ registerTool<TDetails = unknown>(tool: ToolDefinition<TDetails>): void;
5
+ registerCommand(
6
+ name: string,
7
+ options: {
8
+ description?: string;
9
+ handler: (
10
+ args: string,
11
+ ctx: ExtensionCommandContext,
12
+ ) => Promise<void> | void;
13
+ },
14
+ ): void;
15
+ registerProvider(name: string, config: any): void;
16
+ unregisterProvider(name: string): void;
17
+ sendMessage(message: any, options?: any): void;
18
+ sendUserMessage(content: string | any[], options?: any): void;
19
+ appendEntry(customType: string, data?: any): void;
20
+ setSessionName(name: string): void;
21
+ getSessionName(): string | undefined;
22
+ setLabel(entryId: string, label: string | undefined): void;
23
+ getActiveTools(): string[];
24
+ getAllTools(): any[];
25
+ setActiveTools(toolNames: string[]): void;
26
+ }
27
+
28
+ export interface ToolDefinition<TDetails = unknown> {
29
+ name: string;
30
+ label: string;
31
+ description: string;
32
+ promptSnippet?: string;
33
+ promptGuidelines?: string[];
34
+ parameters: any;
35
+ execute(
36
+ params: any,
37
+ signal: AbortSignal | undefined,
38
+ onUpdate: ((update: TDetails) => void) | undefined,
39
+ ctx: ExtensionContext,
40
+ ): Promise<{ type: "text"; content: string; isError?: boolean }>;
41
+ renderCall?: (args: any, theme: any) => any;
42
+ renderResult?: (result: any, options: any, theme: any) => any;
43
+ }
44
+
45
+ export interface ExtensionContext {
46
+ ui: ExtensionUIContext;
47
+ hasUI: boolean;
48
+ cwd: string;
49
+ sessionManager: any;
50
+ model: any;
51
+ isIdle(): boolean;
52
+ abort(): void;
53
+ shutdown(): void;
54
+ getContextUsage():
55
+ | { tokens: number | null; contextWindow: number; percent: number | null }
56
+ | undefined;
57
+ compact(options?: any): void;
58
+ getSystemPrompt(): string;
59
+ }
60
+
61
+ export interface ExtensionUIContext {
62
+ notify(
63
+ message: string,
64
+ type?: "info" | "warning" | "error" | "success",
65
+ ): void;
66
+ }
67
+
68
+ export interface ExtensionCommandContext {
69
+ ui: ExtensionUIContext;
70
+ hasUI: boolean;
71
+ cwd: string;
72
+ sessionManager: any;
73
+ model: any;
74
+ isIdle(): boolean;
75
+ abort(): void;
76
+ shutdown(): void;
77
+ getContextUsage(): any;
78
+ compact(options?: any): void;
79
+ getSystemPrompt(): string;
80
+ waitForIdle(): Promise<void>;
81
+ newSession(options?: any): Promise<{ cancelled: boolean }>;
82
+ fork(entryId: string): Promise<{ cancelled: boolean }>;
83
+ navigateTree(
84
+ targetId: string,
85
+ options?: any,
86
+ ): Promise<{ cancelled: boolean }>;
87
+ switchSession(sessionPath: string): Promise<{ cancelled: boolean }>;
88
+ reload(): Promise<void>;
89
+ }
90
+
91
+ export interface AgentMessage {
92
+ role: "user" | "assistant" | "system" | "tool";
93
+ content?: any;
94
+ id?: string;
95
+ name?: string;
96
+ tool_call_id?: string;
97
+ type?: string;
98
+ compactionEntry?: any;
99
+ timestamp?: number;
100
+ }
3
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-context-map",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
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",