pi-context-map 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-06-15
4
+ ### Critical Fix & Test Suite
5
+ - **Fixed CRASH**: `(pi as any).session?.messages` → now uses event-based message accumulation. `/context-map` no longer crashes with "Cannot read properties of undefined (reading 'messages')".
6
+ - **Fixed Tool Signature**: `registerTool` now uses correct `execute(params, signal, onUpdate, ctx)` signature.
7
+ - **Fixed Import Path**: Uses `pi-coding-agent` (unscoped) instead of `@earendil-works/pi-coding-agent`.
8
+ - **Test Suite**: 34 tests across 5 suites (analyzer, token-counter, insights, generator, live-server).
9
+ - **Type Declarations**: Proper `pi-coding-agent.d.ts` with `ToolDefinition`, `ExtensionCommandContext`, `ExtensionContext`.
10
+ - **Build Clean**: TypeScript strict mode passes with zero errors.
11
+ - **Live SSE Server**: New `LiveReportServer` binds to 127.0.0.1 on a free port and serves the report at `/`.
12
+ - **Auto-Updates**: Server-Sent Events endpoint at `/events` pushes the latest HTML whenever the analysis re-runs (e.g., after each assistant message).
13
+ - **Token Auth**: Each server instance generates a unique session token; the HTML client picks it up via a `<meta>` tag and includes it in the SSE URL to prevent unauthorized access.
14
+ - **Origin Validation**: Only connections from `http://127.0.0.1:<port>` or `http://localhost:<port>` are allowed.
15
+ - **Graceful Shutdown**: `/context-map stop` or `session_shutdown` event stops the server cleanly.
16
+ - **Auto-Refresh**: The `message_end` event triggers an automatic re-analysis when the live server is running, so the browser view stays in sync.
17
+ - **Health & Stop Endpoints**: `/health` for liveness, `POST /stop` for remote termination.
18
+
3
19
  ## [0.3.1] - 2026-06-14
4
20
  ### Design & Interactivity Upgrade
5
21
  - **Linear Design System**: Refactored CSS to use the Linear design tokens (canvas #010102, accent #5e6ad2) for a professional, near-black aesthetic.
package/README.md CHANGED
@@ -48,6 +48,27 @@ The extension categorizes files to help you manage context bloat:
48
48
  3. **Categorization**: It calculates the temporal distance between the current turn and the last file access.
49
49
  4. **Visualization**: It generates a standalone HTML dashboard featuring a stacked composition bar, a file-weight grid with search/filter, and an interactive insights section.
50
50
 
51
+ ## Live Localhost Server
52
+
53
+ When the extension loads, it automatically starts a local HTTP server on `127.0.0.1` (a random free port). The server:
54
+
55
+ - Serves the current report at `http://127.0.0.1:<port>/`.
56
+ - Pushes live updates via Server-Sent Events at `/events?token=<sessionToken>`.
57
+ - Authenticates the SSE connection with a per-session token (injected into the HTML as a `<meta>` tag).
58
+ - Auto-refreshes after each assistant message, so the browser view stays in sync.
59
+
60
+ **Commands:**
61
+
62
+ - `/context-map` — Generate a fresh report and broadcast it to the browser.
63
+ - `/context-map stop` — Stop the live server.
64
+
65
+ **Endpoints:**
66
+
67
+ - `GET /` or `/report.html` — The current report HTML.
68
+ - `GET /events?token=...` — Server-Sent Events stream of updates.
69
+ - `GET /health` — Returns `{ "status": "ok", "port": <number> }`.
70
+ - `POST /stop` — Gracefully stops the server.
71
+
51
72
  ## Design
52
73
 
53
74
  The report uses the **Linear design system** (canvas `#010102`, accent `#5e6ad2`) with **shadcn/ui card patterns**. See `docs/design.md` for the full specification. The output is a single self-contained HTML file with no external dependencies.
@@ -615,6 +615,37 @@ export class ReportGenerator {
615
615
 
616
616
  <script>
617
617
  (function() {
618
+ // ===== Live update via Server-Sent Events =====
619
+ // Token is injected into the script via a meta tag (set by the server).
620
+ // Connect to /events?token=...; when the server pushes a new html payload, replace the document.
621
+ try {
622
+ var tokenMeta = document.querySelector('meta[name="context-map-token"]');
623
+ var token = tokenMeta ? tokenMeta.getAttribute('content') : '';
624
+ var evtSource = new EventSource('/events?token=' + encodeURIComponent(token));
625
+ evtSource.onmessage = function(e) {
626
+ try {
627
+ var payload = JSON.parse(e.data);
628
+ if (payload.html) {
629
+ // Replace the document body with the new HTML
630
+ var parser = new DOMParser();
631
+ var newDoc = parser.parseFromString(payload.html, 'text/html');
632
+ document.documentElement.replaceChild(
633
+ document.importNode(newDoc.documentElement, true),
634
+ document.documentElement
635
+ );
636
+ }
637
+ } catch (err) {
638
+ console.warn('Failed to apply live update:', err);
639
+ }
640
+ };
641
+ evtSource.onerror = function() {
642
+ // Silently close on error; user can refresh the page to reconnect
643
+ evtSource.close();
644
+ };
645
+ } catch (err) {
646
+ // EventSource not available; fall back to manual refresh
647
+ }
648
+
618
649
  // ===== Insight collapse/expand =====
619
650
  document.querySelectorAll('.insight-header[data-toggle]').forEach(function(btn) {
620
651
  btn.addEventListener('click', function() {
@@ -630,7 +661,7 @@ export class ReportGenerator {
630
661
  var grid = document.getElementById('fileGrid');
631
662
  var count = document.getElementById('fileCount');
632
663
  var empty = document.getElementById('emptyState');
633
- var cards = Array.prototype.slice.call(grid.querySelectorAll('.file-card'));
664
+ var cards = grid ? Array.prototype.slice.call(grid.querySelectorAll('.file-card')) : [];
634
665
  var total = cards.length;
635
666
 
636
667
  function applyFilters() {
@@ -1,92 +1,175 @@
1
1
  /**
2
2
  * pi-context-map
3
3
  * Professional Context Profiler for Pi.
4
+ * v0.4.1 — Fixes session.messages crash, tool registration signature, adds tests.
4
5
  */
5
6
 
6
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type {
8
+ ExtensionAPI,
9
+ ExtensionCommandContext,
10
+ AgentMessage,
11
+ } from "pi-coding-agent";
7
12
  import { ContextAnalyzer } from "./analyzer";
8
13
  import { ReportGenerator } from "./generator";
9
14
  import { InsightEngine } from "./insights";
15
+ import { LiveReportServer } from "./live-server";
16
+ import * as path from "node:path";
17
+ import * as os from "node:os";
10
18
 
11
- export default async function piContextMap(pi: ExtensionAPI) {
19
+ const REPORT_PATH = path.join(
20
+ os.homedir(),
21
+ ".pi",
22
+ "context-map",
23
+ "report.html",
24
+ );
25
+
26
+ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
12
27
  const analyzer = new ContextAnalyzer();
28
+ const liveServer = new LiveReportServer();
29
+
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
+ });
13
46
 
14
- async function runAnalysis() {
15
- const messages = (pi as any).session?.messages || [];
16
- const currentTurn = messages.length;
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 : [];
17
53
  const composition = analyzer.analyzeByType(messages, currentTurn);
18
54
  const insights = InsightEngine.generate(composition);
19
55
  const html = ReportGenerator.generateHTML(composition, insights);
20
- const reportPath = ReportGenerator.writeReport(html);
21
- return { composition, insights, reportPath };
56
+
57
+ // Write to disk
58
+ try {
59
+ const fs = await import("node:fs");
60
+ const dir = path.dirname(REPORT_PATH);
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+ fs.writeFileSync(REPORT_PATH, html, "utf8");
65
+ } catch (err: any) {
66
+ console.error(`[pi-context-map] Failed to write report: ${err.message}`);
67
+ }
68
+
69
+ // Push to live server if running
70
+ if (liveServer.isRunning) {
71
+ liveServer.update(html, REPORT_PATH);
72
+ }
73
+
74
+ return { composition, insights, reportPath: REPORT_PATH };
22
75
  }
23
76
 
77
+ // Start live server
78
+ const serverUrl = await liveServer.start();
79
+
80
+ // Register /context-map command
24
81
  pi.registerCommand("context-map", {
25
- description: "Generate a visual context map with actionable insights.",
26
- handler: async (_args: any, ctx: any) => {
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") {
86
+ liveServer.stop();
87
+ ctx.ui.notify("Live server stopped.", "info");
88
+ return;
89
+ }
90
+
27
91
  ctx.ui.notify("Analyzing session context...", "info");
28
92
  try {
29
- const { reportPath, insights } = await runAnalysis();
93
+ const { insights, reportPath } = await runAnalysis();
30
94
  const criticalCount = insights.filter(
31
95
  (i) => i.severity === "critical",
32
96
  ).length;
33
97
  const summary =
34
98
  criticalCount > 0
35
99
  ? `Context map generated. ${criticalCount} critical insight(s) found.`
36
- : `Context map generated successfully.`;
100
+ : "Context map generated successfully.";
101
+ let details = `File: ${reportPath}`;
102
+ if (serverUrl) {
103
+ details += ` | Live: ${serverUrl}`;
104
+ }
37
105
  ctx.ui.notify(
38
- `${summary} Path: ${reportPath}`,
106
+ `${summary} ${details}`,
39
107
  criticalCount > 0 ? "warning" : "success",
40
108
  );
41
- } catch (error) {
42
- const message = error instanceof Error ? error.message : String(error);
43
- ctx.ui.notify(`Failed to generate context map: ${message}`, "error");
109
+ } catch (error: any) {
110
+ ctx.ui.notify(
111
+ `Failed to generate context map: ${error.message}`,
112
+ "error",
113
+ );
44
114
  }
45
115
  },
46
116
  });
47
117
 
118
+ // Register the tool for agent use
48
119
  pi.registerTool({
49
120
  name: "context-map",
121
+ label: "Context Map",
50
122
  description:
51
- "Analyze the current session context composition and return actionable insights.",
123
+ "Analyze the current session context composition and return actionable insights. The live localhost report will auto-update.",
52
124
  parameters: {
53
125
  type: "object",
54
126
  properties: {},
55
127
  },
56
- 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
+ ) {
57
134
  try {
58
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;
59
140
  const summary =
60
141
  `Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
61
142
  `System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
62
143
  `History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
63
144
  `Summaries ${composition.summaries.percent}%. ` +
145
+ `Usage: ${usagePercent}% of typical 128k window. ` +
64
146
  `${insights.length} insight(s) generated.`;
65
147
  return {
66
- summary,
67
- composition: {
68
- system: composition.system.tokens,
69
- tools: composition.tools.tokens,
70
- history: composition.history.tokens,
71
- files: composition.files.tokens,
72
- summaries: composition.summaries.tokens,
73
- total: composition.total.tokens,
74
- },
75
- insights: insights.map((i) => ({
76
- severity: i.severity,
77
- title: i.title,
78
- message: i.message,
79
- command: i.command,
80
- })),
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"),
81
159
  };
82
160
  } catch (error: any) {
83
- return { error: error.message };
161
+ return {
162
+ type: "text" as const,
163
+ content: `Error: ${error.message}`,
164
+ isError: true,
165
+ };
84
166
  }
85
167
  },
86
168
  });
87
169
 
88
- pi.on("session_before_compact", (event: any, ctx: any) => {
89
- const tokens = (event as any).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;
90
173
  if (tokens && tokens > 100_000) {
91
174
  ctx.ui.notify(
92
175
  `High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
@@ -94,4 +177,27 @@ export default async function piContextMap(pi: ExtensionAPI) {
94
177
  );
95
178
  }
96
179
  });
180
+
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) {
184
+ try {
185
+ await runAnalysis();
186
+ } catch {
187
+ // Silently ignore auto-refresh failures
188
+ }
189
+ }
190
+ });
191
+
192
+ // Graceful shutdown
193
+ pi.on("session_shutdown", () => {
194
+ liveServer.stop();
195
+ });
196
+
197
+ if (serverUrl) {
198
+ console.log(`[pi-context-map] Live server running at ${serverUrl}`);
199
+ console.log(
200
+ `[pi-context-map] Run /context-map to generate a report, or /context-map stop to terminate.`,
201
+ );
202
+ }
97
203
  }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * LiveReportServer
3
+ * Serves the context map HTML report on a local HTTP server with live updates via SSE.
4
+ *
5
+ * Features:
6
+ * - Auto-assigns a free port (pass 0 to OS).
7
+ * - Binds to 127.0.0.1 only (no external access).
8
+ * - Serves the current report HTML at `/`.
9
+ * - Streams updates via Server-Sent Events at `/events`.
10
+ * - Graceful shutdown via `stop()`.
11
+ * - Null-safe error handling throughout.
12
+ */
13
+ import * as http from "node:http";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+ import * as crypto from "node:crypto";
18
+ import type { AddressInfo } from "node:net";
19
+
20
+ const DEFAULT_REPORT_PATH = path.join(
21
+ os.homedir(),
22
+ ".pi",
23
+ "context-map",
24
+ "report.html",
25
+ );
26
+
27
+ /**
28
+ * Allowed origins for SSE connections. Only localhost variants are allowed.
29
+ */
30
+ function isAllowedOrigin(origin: string | undefined, port: number): boolean {
31
+ if (!origin) return true; // No Origin header (e.g., direct curl) is allowed
32
+ const allowed = [
33
+ `http://127.0.0.1:${port}`,
34
+ `http://localhost:${port}`,
35
+ "http://127.0.0.1",
36
+ "http://localhost",
37
+ ];
38
+ return allowed.some((o) => origin.startsWith(o));
39
+ }
40
+
41
+ export class LiveReportServer {
42
+ private server: http.Server | null = null;
43
+ private clients: Set<http.ServerResponse> = new Set();
44
+ private currentHtml: string = "";
45
+ private port: number = 0;
46
+ private host: string = "127.0.0.1";
47
+ /** Session token to prevent unauthorized access. */
48
+ public readonly token: string = crypto.randomBytes(16).toString("hex");
49
+
50
+ /**
51
+ * Start the server. Returns a Promise that resolves to the URL, or null on failure.
52
+ */
53
+ public start(): Promise<string | null> {
54
+ if (this.server) {
55
+ return Promise.resolve(this.url);
56
+ }
57
+
58
+ return new Promise((resolve) => {
59
+ try {
60
+ this.server = http.createServer((req, res) =>
61
+ this.handleRequest(req, res),
62
+ );
63
+ this.server.on("error", (err) => {
64
+ console.error(`[pi-context-map] Server error: ${err.message}`);
65
+ this.stop();
66
+ });
67
+
68
+ this.server.listen(0, this.host, () => {
69
+ const addr = this.server?.address() as AddressInfo | null;
70
+ if (addr) {
71
+ this.port = addr.port;
72
+ console.log(`[pi-context-map] Live server: ${this.url}`);
73
+ resolve(this.url);
74
+ } else {
75
+ resolve(null);
76
+ }
77
+ });
78
+ } catch (err: any) {
79
+ console.error(
80
+ `[pi-context-map] Failed to start server: ${err.message}`,
81
+ );
82
+ resolve(null);
83
+ }
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Stop the server and close all client connections.
89
+ */
90
+ public stop(): void {
91
+ if (!this.server) return;
92
+
93
+ // Close all SSE clients
94
+ for (const client of this.clients) {
95
+ try {
96
+ client.end();
97
+ } catch (err) {
98
+ // Ignore errors on close
99
+ }
100
+ }
101
+ this.clients.clear();
102
+
103
+ // Close the server
104
+ this.server.close((err) => {
105
+ if (err) {
106
+ console.error(`[pi-context-map] Error closing server: ${err.message}`);
107
+ }
108
+ });
109
+ this.server = null;
110
+ this.port = 0;
111
+ }
112
+
113
+ /**
114
+ * Update the report content and broadcast to all connected clients.
115
+ * @param html The new HTML content.
116
+ * @param reportPath Optional path to the report file to also write to disk.
117
+ */
118
+ public update(html: string, reportPath?: string): void {
119
+ this.currentHtml = html;
120
+
121
+ // Optionally write to disk
122
+ if (reportPath) {
123
+ try {
124
+ const dir = path.dirname(reportPath);
125
+ if (!fs.existsSync(dir)) {
126
+ fs.mkdirSync(dir, { recursive: true });
127
+ }
128
+ fs.writeFileSync(reportPath, html, "utf8");
129
+ } catch (err: any) {
130
+ console.error(
131
+ `[pi-context-map] Failed to write report: ${err.message}`,
132
+ );
133
+ }
134
+ }
135
+
136
+ // Broadcast to all SSE clients
137
+ for (const client of this.clients) {
138
+ try {
139
+ client.write(
140
+ `data: ${JSON.stringify({ html, timestamp: Date.now() })}\n\n`,
141
+ );
142
+ } catch (err) {
143
+ // Client may have disconnected; remove it
144
+ this.clients.delete(client);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Get the URL the server is listening on, or null if not started.
151
+ */
152
+ public get url(): string | null {
153
+ if (!this.server || this.port === 0) return null;
154
+ return `http://${this.host}:${this.port}`;
155
+ }
156
+
157
+ /**
158
+ * Whether the server is currently running.
159
+ */
160
+ public get isRunning(): boolean {
161
+ return this.server !== null;
162
+ }
163
+
164
+ /**
165
+ * Handle incoming HTTP requests.
166
+ */
167
+ private handleRequest(
168
+ req: http.IncomingMessage,
169
+ res: http.ServerResponse,
170
+ ): void {
171
+ if (!req.url) {
172
+ res.writeHead(400);
173
+ res.end("Bad request");
174
+ return;
175
+ }
176
+
177
+ const url = new URL(req.url, `http://${this.host}:${this.port}`);
178
+
179
+ // SSE endpoint for live updates
180
+ if (url.pathname === "/events") {
181
+ this.handleSSE(req, res);
182
+ return;
183
+ }
184
+
185
+ // Health check
186
+ if (url.pathname === "/health") {
187
+ res.writeHead(200, { "Content-Type": "application/json" });
188
+ res.end(JSON.stringify({ status: "ok", port: this.port }));
189
+ return;
190
+ }
191
+
192
+ // Stop endpoint
193
+ if (url.pathname === "/stop" && req.method === "POST") {
194
+ res.writeHead(200, { "Content-Type": "application/json" });
195
+ res.end(JSON.stringify({ status: "stopping" }));
196
+ setTimeout(() => this.stop(), 100);
197
+ return;
198
+ }
199
+
200
+ // Main page: serve the current HTML or load from disk
201
+ if (url.pathname === "/" || url.pathname === "/report.html") {
202
+ let html = this.currentHtml;
203
+ if (!html) {
204
+ // Try to load from disk as fallback
205
+ try {
206
+ html = fs.readFileSync(DEFAULT_REPORT_PATH, "utf8");
207
+ } catch {
208
+ html = this.placeholderHtml();
209
+ }
210
+ }
211
+ // Inject the session token so the client can authenticate to /events
212
+ if (html.includes("<head>")) {
213
+ html = html.replace(
214
+ "<head>",
215
+ `<head><meta name="context-map-token" content="${this.token}">`,
216
+ );
217
+ }
218
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
219
+ res.end(html);
220
+ return;
221
+ }
222
+
223
+ // 404 for everything else
224
+ res.writeHead(404);
225
+ res.end("Not found");
226
+ }
227
+
228
+ /**
229
+ * Handle Server-Sent Events connection.
230
+ */
231
+ private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {
232
+ // Token-based auth: require ?token=<sessionToken> to prevent unauthorized SSE subscriptions
233
+ if (!req.url) {
234
+ res.writeHead(400);
235
+ res.end("Bad request");
236
+ return;
237
+ }
238
+ const reqUrl = new URL(req.url, `http://${this.host}:${this.port}`);
239
+ const providedToken = reqUrl.searchParams.get("token");
240
+ if (providedToken !== this.token) {
241
+ res.writeHead(401, { "Content-Type": "text/plain" });
242
+ res.end("Unauthorized: invalid or missing token");
243
+ return;
244
+ }
245
+
246
+ // Origin validation: only allow connections from localhost
247
+ if (!isAllowedOrigin(req.headers.origin, this.port)) {
248
+ res.writeHead(403, { "Content-Type": "text/plain" });
249
+ res.end("Forbidden: origin not allowed");
250
+ return;
251
+ }
252
+
253
+ res.writeHead(200, {
254
+ "Content-Type": "text/event-stream",
255
+ "Cache-Control": "no-cache",
256
+ Connection: "keep-alive",
257
+ "Access-Control-Allow-Origin": `http://127.0.0.1:${this.port}`,
258
+ });
259
+
260
+ // Send initial state if we have content
261
+ if (this.currentHtml) {
262
+ res.write(
263
+ `data: ${JSON.stringify({ html: this.currentHtml, timestamp: Date.now() })}\n\n`,
264
+ );
265
+ } else {
266
+ res.write(`data: ${JSON.stringify({ waiting: true })}\n\n`);
267
+ }
268
+
269
+ this.clients.add(res);
270
+
271
+ // Heartbeat to keep connection alive (every 30s)
272
+ const heartbeat = setInterval(() => {
273
+ try {
274
+ res.write(": heartbeat\n\n");
275
+ } catch {
276
+ clearInterval(heartbeat);
277
+ this.clients.delete(res);
278
+ }
279
+ }, 30000);
280
+
281
+ req.on("close", () => {
282
+ clearInterval(heartbeat);
283
+ this.clients.delete(res);
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Placeholder HTML shown when no report has been generated yet.
289
+ */
290
+ private placeholderHtml(): string {
291
+ return `<!DOCTYPE html>
292
+ <html><head><title>pi-context-map</title>
293
+ <style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#010102;color:#f7f8f8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;}</style>
294
+ </head><body>
295
+ <div style="text-align:center;">
296
+ <h1 style="color:#5e6ad2;font-size:24px;font-weight:600;">pi-context-map</h1>
297
+ <p style="color:#8a8f98;margin-top:8px;">No report generated yet. Run <code>/context-map</code> in Pi to generate one.</p>
298
+ </div>
299
+ </body></html>`;
300
+ }
301
+ }
@@ -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.3.1",
3
+ "version": "0.4.1",
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",