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.
- package/CHANGELOG.md +23 -9
- package/extensions/generator.ts +603 -670
- package/extensions/index.ts +110 -54
- package/extensions/live-server.ts +25 -8
- package/extensions/types/pi-coding-agent.d.ts +100 -2
- package/package.json +1 -1
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,19 @@ 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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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(
|
|
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();
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 "
|
|
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.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",
|