pi-context-map 0.4.0 → 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 +8 -2
- package/extensions/index.ts +103 -51
- package/extensions/live-server.ts +25 -7
- package/extensions/types/pi-coding-agent.d.ts +100 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.4.
|
|
4
|
-
###
|
|
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.
|
|
5
11
|
- **Live SSE Server**: New `LiveReportServer` binds to 127.0.0.1 on a free port and serves the report at `/`.
|
|
6
12
|
- **Auto-Updates**: Server-Sent Events endpoint at `/events` pushes the latest HTML whenever the analysis re-runs (e.g., after each assistant message).
|
|
7
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.
|
package/extensions/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-context-map
|
|
3
3
|
* Professional Context Profiler for Pi.
|
|
4
|
-
* v0.4.
|
|
4
|
+
* v0.4.1 — Fixes session.messages crash, tool registration signature, adds tests.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
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(
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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))
|
|
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
|
|
66
|
+
console.error(`[pi-context-map] Failed to write report: ${err.message}`);
|
|
36
67
|
}
|
|
37
68
|
|
|
38
|
-
// Push to live server
|
|
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
|
|
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:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
105
|
+
ctx.ui.notify(
|
|
106
|
+
`${summary} ${details}`,
|
|
107
|
+
criticalCount > 0 ? "warning" : "success",
|
|
108
|
+
);
|
|
72
109
|
} catch (error: any) {
|
|
73
|
-
ctx.ui.notify(
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 {
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
131
|
-
pi.on("message_end", async (
|
|
132
|
-
if (
|
|
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,15 @@ export default async function piContextMap(pi: ExtensionAPI) {
|
|
|
138
189
|
}
|
|
139
190
|
});
|
|
140
191
|
|
|
141
|
-
// Graceful shutdown
|
|
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
197
|
if (serverUrl) {
|
|
148
198
|
console.log(`[pi-context-map] Live server running at ${serverUrl}`);
|
|
149
|
-
console.log(
|
|
199
|
+
console.log(
|
|
200
|
+
`[pi-context-map] Run /context-map to generate a report, or /context-map stop to terminate.`,
|
|
201
|
+
);
|
|
150
202
|
}
|
|
151
203
|
}
|
|
@@ -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(
|
|
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) =>
|
|
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();
|
|
@@ -69,7 +76,9 @@ export class LiveReportServer {
|
|
|
69
76
|
}
|
|
70
77
|
});
|
|
71
78
|
} catch (err: any) {
|
|
72
|
-
console.error(
|
|
79
|
+
console.error(
|
|
80
|
+
`[pi-context-map] Failed to start server: ${err.message}`,
|
|
81
|
+
);
|
|
73
82
|
resolve(null);
|
|
74
83
|
}
|
|
75
84
|
});
|
|
@@ -118,14 +127,18 @@ export class LiveReportServer {
|
|
|
118
127
|
}
|
|
119
128
|
fs.writeFileSync(reportPath, html, "utf8");
|
|
120
129
|
} catch (err: any) {
|
|
121
|
-
console.error(
|
|
130
|
+
console.error(
|
|
131
|
+
`[pi-context-map] Failed to write report: ${err.message}`,
|
|
132
|
+
);
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
// Broadcast to all SSE clients
|
|
126
137
|
for (const client of this.clients) {
|
|
127
138
|
try {
|
|
128
|
-
client.write(
|
|
139
|
+
client.write(
|
|
140
|
+
`data: ${JSON.stringify({ html, timestamp: Date.now() })}\n\n`,
|
|
141
|
+
);
|
|
129
142
|
} catch (err) {
|
|
130
143
|
// Client may have disconnected; remove it
|
|
131
144
|
this.clients.delete(client);
|
|
@@ -151,7 +164,10 @@ export class LiveReportServer {
|
|
|
151
164
|
/**
|
|
152
165
|
* Handle incoming HTTP requests.
|
|
153
166
|
*/
|
|
154
|
-
private handleRequest(
|
|
167
|
+
private handleRequest(
|
|
168
|
+
req: http.IncomingMessage,
|
|
169
|
+
res: http.ServerResponse,
|
|
170
|
+
): void {
|
|
155
171
|
if (!req.url) {
|
|
156
172
|
res.writeHead(400);
|
|
157
173
|
res.end("Bad request");
|
|
@@ -243,7 +259,9 @@ export class LiveReportServer {
|
|
|
243
259
|
|
|
244
260
|
// Send initial state if we have content
|
|
245
261
|
if (this.currentHtml) {
|
|
246
|
-
res.write(
|
|
262
|
+
res.write(
|
|
263
|
+
`data: ${JSON.stringify({ html: this.currentHtml, timestamp: Date.now() })}\n\n`,
|
|
264
|
+
);
|
|
247
265
|
} else {
|
|
248
266
|
res.write(`data: ${JSON.stringify({ waiting: true })}\n\n`);
|
|
249
267
|
}
|
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
declare module "
|
|
2
|
-
export
|
|
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.
|
|
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",
|