pi-context-map 0.1.0

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/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # pi-context-map
2
+
3
+ A Pi extension that transforms your session's context window into a visual, actionable dashboard.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **Visual Context Budget**: See exactly how your tokens are distributed between system prompts, history, files, and tool results.
8
+ - **Working Set Analysis**: Identify which files are "Active", "Stale", or "Legacy".
9
+ - **Token Weighting**: Discover which files are consuming the most context window space.
10
+ - **Operation History**: Track how files entered the context (Read 👁️, Write 📝, Edit ✍️).
11
+ - **Temporal Mapping**: See a timeline of file access to identify candidates for compaction.
12
+
13
+ ## 🛠️ Installation
14
+
15
+ ```bash
16
+ pi install npm:pi-context-map
17
+ ```
18
+
19
+ ## 📖 Usage
20
+
21
+ Run the following command in any Pi session:
22
+
23
+ `/context-map`
24
+
25
+ The extension will analyze your session and generate a standalone HTML report at:
26
+ `~/.pi/context-map/report.html`
27
+
28
+ ## 📊 How it Works
29
+
30
+ The extension scans the session's message history to build a map of the "Working Set":
31
+ 1. **Scanning**: Every `tool_use` call for `read`, `write`, or `edit` is tracked.
32
+ 2. **Weighting**: Content length is converted to estimated tokens.
33
+ 3. **Categorization**:
34
+ - **Active**: Accessed in the last 3 turns.
35
+ - **Stale**: Accessed in the last 10 turns.
36
+ - **Legacy**: Accessed > 10 turns ago.
37
+ 4. **Visualization**: Data is injected into a high-performance HTML dashboard.
38
+
39
+ ## ⚖️ License
40
+
41
+ MIT
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ContextAnalyzer
3
+ * Responsible for parsing Pi session messages to identify the active working set of files,
4
+ * their token weights, and their temporal status.
5
+ */
6
+ export interface FileOp {
7
+ type: "read" | "write" | "edit" | "delete";
8
+ turn: number;
9
+ timestamp: number;
10
+ }
11
+ export interface FileContext {
12
+ path: string;
13
+ weight: number;
14
+ lastOp: FileOp;
15
+ status: "active" | "stale" | "legacy";
16
+ }
17
+ export interface ContextMap {
18
+ files: FileContext[];
19
+ totalTokens: number;
20
+ systemTokens: number;
21
+ historyTokens: number;
22
+ fileTokens: number;
23
+ toolTokens: number;
24
+ }
25
+ export declare class ContextAnalyzer {
26
+ /**
27
+ * Heuristic for token estimation: approx 4 chars per token.
28
+ */
29
+ private static TOKEN_HEURISTIC;
30
+ /**
31
+ * Analyze session messages to produce a context map.
32
+ * @param messages The full session conversation history.
33
+ * @param currentTurn The current turn number.
34
+ */
35
+ analyze(messages: any[], currentTurn: number): ContextMap;
36
+ private extractPath;
37
+ private getOpType;
38
+ private calculateStatus;
39
+ private findToolResult;
40
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * ContextAnalyzer
4
+ * Responsible for parsing Pi session messages to identify the active working set of files,
5
+ * their token weights, and their temporal status.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.ContextAnalyzer = void 0;
9
+ class ContextAnalyzer {
10
+ /**
11
+ * Heuristic for token estimation: approx 4 chars per token.
12
+ */
13
+ static TOKEN_HEURISTIC = 4;
14
+ /**
15
+ * Analyze session messages to produce a context map.
16
+ * @param messages The full session conversation history.
17
+ * @param currentTurn The current turn number.
18
+ */
19
+ analyze(messages, currentTurn) {
20
+ const fileRegistry = new Map();
21
+ let totalTokens = 0;
22
+ let fileTokens = 0;
23
+ let toolTokens = 0;
24
+ messages.forEach((msg, index) => {
25
+ const turn = index + 1;
26
+ // Basic token estimation for the message
27
+ const msgText = typeof msg.content === "string"
28
+ ? msg.content
29
+ : JSON.stringify(msg.content);
30
+ const msgTokens = Math.ceil(msgText.length / ContextAnalyzer.TOKEN_HEURISTIC);
31
+ totalTokens += msgTokens;
32
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
33
+ for (const block of msg.content) {
34
+ if (block.type === "tool_use") {
35
+ const input = block.input;
36
+ const path = this.extractPath(block.name, input);
37
+ if (path) {
38
+ const opType = this.getOpType(block.name);
39
+ // If the file is already tracked, update it
40
+ // Find the tool result for this tool use to get actual content length
41
+ const result = this.findToolResult(messages, index, block.id);
42
+ const content = result?.content || "";
43
+ const weight = Math.ceil(String(content).length / ContextAnalyzer.TOKEN_HEURISTIC);
44
+ fileRegistry.set(path, {
45
+ path,
46
+ weight,
47
+ lastOp: {
48
+ type: opType,
49
+ turn,
50
+ timestamp: msg.timestamp || Date.now(),
51
+ },
52
+ status: this.calculateStatus(turn, currentTurn),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if (msg.role === "tool") {
59
+ toolTokens += Math.ceil(String(msg.content).length / ContextAnalyzer.TOKEN_HEURISTIC);
60
+ }
61
+ });
62
+ const files = Array.from(fileRegistry.values());
63
+ fileTokens = files.reduce((acc, f) => acc + f.weight, 0);
64
+ return {
65
+ files: files.sort((a, b) => b.weight - a.weight).slice(0, 100),
66
+ totalTokens,
67
+ systemTokens: 0, // Pi provides this via ctx, not messages
68
+ historyTokens: totalTokens - fileTokens - toolTokens,
69
+ fileTokens,
70
+ toolTokens,
71
+ };
72
+ }
73
+ extractPath(toolName, input) {
74
+ if (toolName === "read" || toolName === "write" || toolName === "edit") {
75
+ return typeof input.path === "string" ? input.path : null;
76
+ }
77
+ if (toolName === "bash") {
78
+ // Simple regex for paths in bash commands (e.g., cat path/to/file)
79
+ const match = input.command?.match(/(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/);
80
+ return match ? match[1] : null;
81
+ }
82
+ return null;
83
+ }
84
+ getOpType(toolName) {
85
+ switch (toolName) {
86
+ case "write":
87
+ return "write";
88
+ case "edit":
89
+ return "edit";
90
+ case "bash":
91
+ return "delete"; // Simplified; usually bash implies modification or deletion
92
+ default:
93
+ return "read";
94
+ }
95
+ }
96
+ calculateStatus(turn, currentTurn) {
97
+ const diff = currentTurn - turn;
98
+ if (diff <= 3)
99
+ return "active";
100
+ if (diff <= 10)
101
+ return "stale";
102
+ return "legacy";
103
+ }
104
+ findToolResult(messages, toolTurnIndex, toolId) {
105
+ // Look for the tool result immediately following the tool use
106
+ for (let i = toolTurnIndex + 1; i < messages.length; i++) {
107
+ if (messages[i].role === "tool" && messages[i].tool_call_id === toolId) {
108
+ return messages[i];
109
+ }
110
+ // If we hit another assistant turn, the result for this specific call is likely gone/compacted
111
+ if (messages[i].role === "assistant")
112
+ break;
113
+ }
114
+ return null;
115
+ }
116
+ }
117
+ exports.ContextAnalyzer = ContextAnalyzer;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ReportGenerator
3
+ * Generates a visual HTML dashboard based on the ContextMap.
4
+ */
5
+ import type { ContextMap } from "./analyzer";
6
+ export declare class ReportGenerator {
7
+ static generateHTML(map: ContextMap): string;
8
+ static writeReport(html: string): string;
9
+ private static getOpIcon;
10
+ private static escapeHtml;
11
+ }
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ /**
3
+ * ReportGenerator
4
+ * Generates a visual HTML dashboard based on the ContextMap.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ReportGenerator = void 0;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const node_os_1 = require("node:os");
11
+ class ReportGenerator {
12
+ static generateHTML(map) {
13
+ const fileCards = map.files
14
+ .map((file) => `
15
+ <div class="file-card ${file.status}">
16
+ <div class="file-header">
17
+ <span class="file-path">${ReportGenerator.escapeHtml(file.path)}</span>
18
+ <span class="file-weight">${file.weight.toLocaleString()} tokens</span>
19
+ </div>
20
+ <div class="file-footer">
21
+ <span class="op-badge">${ReportGenerator.getOpIcon(file.lastOp.type)} ${file.lastOp.type}</span>
22
+ <span class="turn-badge">Turn ${file.lastOp.turn}</span>
23
+ <span class="status-text">${file.status.toUpperCase()}</span>
24
+ </div>
25
+ <div class="weight-bar">
26
+ <div class="weight-fill" style="width: ${Math.min(100, (file.weight / 1000) * 100)}%"></div>
27
+ </div>
28
+ </div>
29
+ `)
30
+ .join("");
31
+ const budgetPercent = (map.fileTokens / map.totalTokens) * 100;
32
+ return `
33
+ <!DOCTYPE html>
34
+ <html lang="en">
35
+ <head>
36
+ <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
+ <title>Pi Context Map</title>
39
+ <style>
40
+ :root {
41
+ --bg: #0f172a;
42
+ --card-bg: #1e293b;
43
+ --text: #f1f5f9;
44
+ --text-dim: #94a3b8;
45
+ --primary: #38bdf8;
46
+ --active: #22c55e;
47
+ --stale: #eab308;
48
+ --legacy: #ef4444;
49
+ --border: #334155;
50
+ }
51
+ body {
52
+ background: var(--bg);
53
+ color: var(--text);
54
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
55
+ margin: 0;
56
+ padding: 2rem;
57
+ line-height: 1.5;
58
+ }
59
+ .container { max-width: 1200px; margin: 0 auto; }
60
+ header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 2rem; }
61
+ h1 { font-size: 2rem; margin: 0; color: var(--primary); }
62
+ .stats-grid {
63
+ display: grid;
64
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
65
+ gap: 1.5rem;
66
+ margin-top: 2rem;
67
+ }
68
+ .stat-card {
69
+ background: var(--card-bg);
70
+ padding: 1.5rem;
71
+ border-radius: 12px;
72
+ border: 1px solid var(--border);
73
+ text-align: center;
74
+ }
75
+ .stat-value { font-size: 1.5rem; font-weight: bold; display: block; }
76
+ .stat-label { color: var(--text-dim); font-size: 0.875rem; text-transform: uppercase; }
77
+
78
+ .budget-container {
79
+ margin: 2rem 0;
80
+ background: var(--card-bg);
81
+ padding: 1rem;
82
+ border-radius: 12px;
83
+ border: 1px solid var(--border);
84
+ }
85
+ .budget-bar {
86
+ height: 24px;
87
+ background: #020617;
88
+ border-radius: 12px;
89
+ display: flex;
90
+ overflow: hidden;
91
+ margin-bottom: 0.5rem;
92
+ }
93
+ .budget-segment { height: 100%; transition: width 0.3s ease; }
94
+ .seg-system { background: #6366f1; }
95
+ .seg-history { background: #a855f7; }
96
+ .seg-files { background: var(--primary); }
97
+ .seg-tools { background: #ec4899; }
98
+
99
+ .budget-legend {
100
+ display: flex;
101
+ gap: 1rem;
102
+ justify-content: center;
103
+ font-size: 0.75rem;
104
+ color: var(--text-dim);
105
+ }
106
+ .legend-item { display: flex; align-items: center; gap: 0.5rem; }
107
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
108
+
109
+ .file-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
112
+ gap: 1rem;
113
+ }
114
+ .file-card {
115
+ background: var(--card-bg);
116
+ border: 1px solid var(--border);
117
+ border-radius: 12px;
118
+ padding: 1rem;
119
+ transition: transform 0.2s ease, border-color 0.2s ease;
120
+ display: flex;
121
+ flex-direction: column;
122
+ justify-content: space-between;
123
+ }
124
+ .file-card:hover { transform: translateY(-4px); border-color: var(--primary); }
125
+ .file-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: flex-start;
129
+ margin-bottom: 1rem;
130
+ }
131
+ .file-path {
132
+ font-family: 'Fira Code', monospace;
133
+ font-size: 0.875rem;
134
+ word-break: break-all;
135
+ margin-right: 1rem;
136
+ color: var(--text);
137
+ }
138
+ .file-weight { font-size: 0.75rem; color: var(--text-dim); white-space: nowrap; }
139
+ .file-footer {
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: center;
143
+ margin-top: 1rem;
144
+ font-size: 0.75rem;
145
+ }
146
+ .op-badge {
147
+ background: #0f172a;
148
+ padding: 2px 6px;
149
+ border-radius: 4px;
150
+ color: var(--text-dim);
151
+ }
152
+ .turn-badge { color: var(--text-dim); }
153
+ .status-text { font-weight: bold; text-transform: uppercase; }
154
+
155
+ /* Status Colors */
156
+ .active { border-left: 4px solid var(--active); }
157
+ .active .status-text { color: var(--active); }
158
+ .stale { border-left: 4px solid var(--stale); }
159
+ .stale .status-text { color: var(--stale); }
160
+ .legacy { border-left: 4px solid var(--legacy); }
161
+ .legacy .status-text { color: var(--legacy); }
162
+
163
+ .weight-bar {
164
+ height: 4px;
165
+ background: #020617;
166
+ border-radius: 2px;
167
+ margin-top: 1rem;
168
+ overflow: hidden;
169
+ }
170
+ .weight-fill {
171
+ height: 100%;
172
+ background: var(--primary);
173
+ transition: width 0.3s ease;
174
+ }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <div class="container">
179
+ <header>
180
+ <h1>Pi Context Map</h1>
181
+ <p style="color: var(--text-dim)">Session context window visualization and token distribution.</p>
182
+
183
+ <div class="stats-grid">
184
+ <div class="stat-card">
185
+ <span class="stat-value">${map.totalTokens.toLocaleString()}</span>
186
+ <span class="stat-label">Total Tokens</span>
187
+ </div>
188
+ <div class="stat-card">
189
+ <span class="stat-value">${map.files.length}</span>
190
+ <span class="stat-label">Files in Context</span>
191
+ </div>
192
+ <div class="stat-card">
193
+ <span class="stat-value">${map.fileTokens.toLocaleString()}</span>
194
+ <span class="stat-label">File Tokens</span>
195
+ </div>
196
+ <div class="stat-card">
197
+ <span class="stat-value">${Math.round(budgetPercent)}%</span>
198
+ <span class="stat-label">File Load</span>
199
+ </div>
200
+ </div>
201
+
202
+ <div class="budget-container">
203
+ <div class="budget-bar">
204
+ <div class="budget-segment seg-system" style="width: ${(map.systemTokens / map.totalTokens) * 100 || 0}%"></div>
205
+ <div class="budget-segment seg-history" style="width: ${(map.historyTokens / map.totalTokens) * 100 || 0}%"></div>
206
+ <div class="budget-segment seg-files" style="width: ${(map.fileTokens / map.totalTokens) * 100 || 0}%"></div>
207
+ <div class="budget-segment seg-tools" style="width: ${(map.toolTokens / map.totalTokens) * 100 || 0}%"></div>
208
+ </div>
209
+ <div class="budget-legend">
210
+ <div class="legend-item"><span class="dot seg-system"></span> System</div>
211
+ <div class="legend-item"><span class="dot seg-history"></span> History</div>
212
+ <div class="legend-item"><span class="dot seg-files"></span> Files</div>
213
+ <div class="legend-item"><span class="dot seg-tools"></span> Tools</div>
214
+ </div>
215
+ </div>
216
+ </header>
217
+
218
+ <div class="file-grid">
219
+ ${fileCards}
220
+ </div>
221
+ </div>
222
+ </body>
223
+ </html>
224
+ `;
225
+ }
226
+ static writeReport(html) {
227
+ const reportDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".pi", "context-map");
228
+ (0, node_fs_1.mkdirSync)(reportDir, { recursive: true });
229
+ const reportPath = (0, node_path_1.join)(reportDir, "report.html");
230
+ (0, node_fs_1.writeFileSync)(reportPath, html, "utf8");
231
+ return reportPath;
232
+ }
233
+ static getOpIcon(type) {
234
+ switch (type) {
235
+ case "read":
236
+ return "👁️";
237
+ case "write":
238
+ return "📝";
239
+ case "edit":
240
+ return "✍️";
241
+ case "delete":
242
+ return "🗑️";
243
+ default:
244
+ return "📄";
245
+ }
246
+ }
247
+ static escapeHtml(text) {
248
+ const map = {
249
+ "&": "&amp;",
250
+ "<": "&lt;",
251
+ ">": "&gt;",
252
+ '"': "&quot;",
253
+ "'": "&#039;",
254
+ };
255
+ return text.replace(/[&<>"']/g, (m) => map[m]);
256
+ }
257
+ }
258
+ exports.ReportGenerator = ReportGenerator;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * pi-context-map
3
+ * Pi extension to visualize session context window and token distribution.
4
+ */
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ export default function (pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * pi-context-map
4
+ * Pi extension to visualize session context window and token distribution.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.default = default_1;
8
+ const analyzer_1 = require("./analyzer");
9
+ const generator_1 = require("./generator");
10
+ function default_1(pi) {
11
+ const analyzer = new analyzer_1.ContextAnalyzer();
12
+ // Register the /context-map command
13
+ pi.registerCommand("context-map", {
14
+ description: "Generate a visual map of the current session context window.",
15
+ handler: (_args, ctx) => {
16
+ ctx.ui.notify("Analyzing session context...", "info");
17
+ try {
18
+ // 1. Extract messages and current turn
19
+ // Note: We assume ctx.session.messages is available.
20
+ // If not, we may need to fetch them via another API or use provided event data.
21
+ const messages = ctx.session.messages || [];
22
+ const currentTurn = messages.length;
23
+ if (messages.length === 0) {
24
+ ctx.ui.notify("No session history found to map.", "warning");
25
+ return;
26
+ }
27
+ // 2. Analyze context
28
+ const map = analyzer.analyze(messages, currentTurn);
29
+ // 3. Generate HTML Report
30
+ const html = generator_1.ReportGenerator.generateHTML(map);
31
+ const reportPath = generator_1.ReportGenerator.writeReport(html);
32
+ ctx.ui.notify(`Context map generated successfully! \nPath: ${reportPath}`, "success");
33
+ // Providing a link or instruction to open the report
34
+ ctx.ui.notify("You can open the report.html in your browser to see the visualization.", "info");
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ ctx.ui.notify(`Failed to generate context map: ${message}`, "error");
39
+ }
40
+ },
41
+ });
42
+ // Optional: Notify the user when a significant amount of context is loaded
43
+ pi.on("session_before_compact", (event, ctx) => {
44
+ const { preparation } = event;
45
+ const tokens = preparation.tokensBefore;
46
+ if (tokens > 100_000) {
47
+ ctx.ui.notify(`High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`, "info");
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,41 @@
1
+ # Proposal: pi-context-map
2
+
3
+ ## 1. Problem Statement
4
+ As Pi sessions grow in complexity, the "Context Window" becomes a black box. Users and agents often lose track of:
5
+ - Which files are currently consuming the most tokens.
6
+ - When a file was last "refreshed" (read) by the agent.
7
+ - The distribution of tokens between history, system prompts, and tool outputs.
8
+
9
+ This leads to "context bloat," where the agent becomes sluggish or forgets critical instructions because the window is filled with stale file content.
10
+
11
+ ## 2. Goal
12
+ Create a Pi extension that provides a **real-time, visual map of the current session's context**. It should transform the abstract concept of a "token window" into a concrete, actionable dashboard.
13
+
14
+ ## 3. Core Features
15
+ ### A. `/context-map` Command
16
+ A command that generates a visual report of the current context.
17
+
18
+ ### B. Context Analysis
19
+ - **File Inventory**: List all files currently in context.
20
+ - **Weight Tracking**: Approximate token count per file.
21
+ - **Status Mapping**:
22
+ - `Active`: Read/Modified in the last 3 turns.
23
+ - `Stale`: Read 4-10 turns ago.
24
+ - `Legacy`: Read >10 turns ago (candidate for compaction).
25
+ - **Operation History**: Mark if a file was Read 🟢, Written 🟠, or Edited 🟡.
26
+
27
+ ### C. Visual Output
28
+ The extension will generate an `index.html` report (stored in `.pi/context-map/report.html`) featuring:
29
+ - **Token Budget Bar**: Visual breakdown of context usage.
30
+ - **File Treemap/List**: Files sized by their token weight.
31
+ - **Temporal Timeline**: A simple timeline showing when files entered the context.
32
+
33
+ ## 4. Success Criteria
34
+ - The user can run `/context-map` and immediately see which file is the "token hog."
35
+ - The user can identify "stale" files that can be removed to free up space.
36
+ - Zero performance degradation during normal session operation.
37
+ - Full compatibility with `pi-ultra-compact` (since both manage context).
38
+
39
+ ## 5. Non-Goals
40
+ - This is a *visualization* tool, not an *automatic* context cleaner (though it provides the data needed for a user to trigger compaction).
41
+ - It will not modify the actual LLM context window, only report on it.
package/docs/spec.md ADDED
@@ -0,0 +1,57 @@
1
+ # Technical Specification: pi-context-map
2
+
3
+ ## 1. Architecture Overview
4
+ `pi-context-map` is a Pi extension that analyzes the session message history to derive a "map" of the active context. It operates as a read-only analysis tool triggered by a user command.
5
+
6
+ ## 2. Data Extraction Logic
7
+ When `/context-map` is called, the extension will:
8
+ 1. **Scan Messages**: Iterate through all messages in the current session history.
9
+ 2. **Identify File Ops**:
10
+ - Scan `tool_use` blocks for `read`, `write`, `edit`, and `bash` (regex for file paths).
11
+ - Map each file to its most recent occurrence (turn number).
12
+ 3. **Calculate Weights**:
13
+ - For each file found, retrieve its content length from the corresponding `tool_result` if available.
14
+ - Estimate tokens using a heuristic: $\text{tokens} \approx \text{chars} / 4$.
15
+ 4. **Assign Status**:
16
+ - **Active**: Turn difference $\le 3$.
17
+ - **Stale**: $3 <$ Turn difference $\le 10$.
18
+ - **Legacy**: Turn difference $> 10$.
19
+
20
+ ## 3. Component Design
21
+
22
+ ### A. `ContextAnalyzer` (Class)
23
+ - `analyze(messages: Message[])`: Returns a `ContextMap` object.
24
+ - `calculateTokens(text: string)`: Returns estimated token count.
25
+ - `getFileMetadata(path: string)`: Tracks the operation type and timestamp.
26
+
27
+ ### B. `ReportGenerator` (Class)
28
+ - `generateHTML(map: ContextMap)`: Produces a standalone HTML string.
29
+ - `writeReport(html: string)`: Saves the report to `.pi/context-map/report.html` and opens it.
30
+
31
+ ### C. `ExtensionEntry` (Main)
32
+ - `pi.registerCommand("context-map", ...)`: The entry point.
33
+ - `pi.on("session_before_compact", ...)`: (Optional) Could trigger a map update before compaction.
34
+
35
+ ## 4. Visual Specification (HTML Dashboard)
36
+ The report will be a single-file HTML dashboard with:
37
+ - **Header**: Session ID, Total Estimated Tokens, and Timestamp.
38
+ - **Context Budget**: A CSS-based stacked bar showing:
39
+ - `[ System ] [ History ] [ Files ] [ Tool Outputs ]`
40
+ - **File Grid**:
41
+ - Cards for each file.
42
+ - Size proportional to token weight.
43
+ - Color-coded by status (Green $\to$ Yellow $\to$ Red).
44
+ - Icons for operation type (👁️ for read, ✍️ for edit).
45
+ - **Stats Table**:
46
+ - File Path | Tokens | Last Turn | Status.
47
+
48
+ ## 5. Implementation Details
49
+ - **Language**: TypeScript.
50
+ - **Dependencies**: `@earendil-works/pi-coding-agent`, `node:fs`, `node:path`.
51
+ - **Complexity**:
52
+ - Time: $O(N)$ where $N$ is number of messages.
53
+ - Space: $O(F)$ where $F$ is number of unique files in context.
54
+
55
+ ## 6. Error Handling
56
+ - **Missing Tool Results**: If a file was read but the result is missing (e.g., due to previous compaction), mark weight as "Unknown" and status as "Stale".
57
+ - **Large Repos**: Limit the map to the top 100 largest files to prevent the HTML report from crashing the browser.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "pi-context-map",
3
+ "version": "0.1.0",
4
+ "description": "A Pi extension that visualizes the current session context window and token distribution.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc -w"
10
+ },
11
+ "keywords": [
12
+ "pi",
13
+ "extension",
14
+ "context",
15
+ "visualization",
16
+ "tokens"
17
+ ],
18
+ "author": "ZachDreamZ",
19
+ "license": "MIT",
20
+ "pi": {
21
+ "extensions": [
22
+ {
23
+ "name": "context-map",
24
+ "entry": "dist/index.js",
25
+ "description": "Visualizes the session context window and token distribution."
26
+ }
27
+ ]
28
+ },
29
+ "dependencies": {},
30
+ "devDependencies": {
31
+ "typescript": "^5.0.0",
32
+ "@types/node": "^20.0.0"
33
+ }
34
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ContextAnalyzer
3
+ * Responsible for parsing Pi session messages to identify the active working set of files,
4
+ * their token weights, and their temporal status.
5
+ */
6
+
7
+ export interface FileOp {
8
+ type: "read" | "write" | "edit" | "delete";
9
+ turn: number;
10
+ timestamp: number;
11
+ }
12
+
13
+ export interface FileContext {
14
+ path: string;
15
+ weight: number; // Estimated tokens
16
+ lastOp: FileOp;
17
+ status: "active" | "stale" | "legacy";
18
+ }
19
+
20
+ export interface ContextMap {
21
+ files: FileContext[];
22
+ totalTokens: number;
23
+ systemTokens: number;
24
+ historyTokens: number;
25
+ fileTokens: number;
26
+ toolTokens: number;
27
+ }
28
+
29
+ export class ContextAnalyzer {
30
+ /**
31
+ * Heuristic for token estimation: approx 4 chars per token.
32
+ */
33
+ private static TOKEN_HEURISTIC = 4;
34
+
35
+ /**
36
+ * Analyze session messages to produce a context map.
37
+ * @param messages The full session conversation history.
38
+ * @param currentTurn The current turn number.
39
+ */
40
+ public analyze(messages: any[], currentTurn: number): ContextMap {
41
+ const fileRegistry = new Map<string, FileContext>();
42
+ let totalTokens = 0;
43
+ let fileTokens = 0;
44
+ let toolTokens = 0;
45
+
46
+ messages.forEach((msg, index) => {
47
+ const turn = index + 1;
48
+
49
+ // Basic token estimation for the message
50
+ const msgText =
51
+ typeof msg.content === "string"
52
+ ? msg.content
53
+ : JSON.stringify(msg.content);
54
+ const msgTokens = Math.ceil(
55
+ msgText.length / ContextAnalyzer.TOKEN_HEURISTIC,
56
+ );
57
+ totalTokens += msgTokens;
58
+
59
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
60
+ for (const block of msg.content) {
61
+ if (block.type === "tool_use") {
62
+ const input = block.input as Record<string, any>;
63
+ const path = this.extractPath(block.name, input);
64
+
65
+ if (path) {
66
+ const opType = this.getOpType(block.name);
67
+
68
+ // If the file is already tracked, update it
69
+
70
+ // Find the tool result for this tool use to get actual content length
71
+ const result = this.findToolResult(messages, index, block.id);
72
+ const content = result?.content || "";
73
+ const weight = Math.ceil(
74
+ String(content).length / ContextAnalyzer.TOKEN_HEURISTIC,
75
+ );
76
+
77
+ fileRegistry.set(path, {
78
+ path,
79
+ weight,
80
+ lastOp: {
81
+ type: opType,
82
+ turn,
83
+ timestamp: msg.timestamp || Date.now(),
84
+ },
85
+ status: this.calculateStatus(turn, currentTurn),
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ if (msg.role === "tool") {
93
+ toolTokens += Math.ceil(
94
+ String(msg.content).length / ContextAnalyzer.TOKEN_HEURISTIC,
95
+ );
96
+ }
97
+ });
98
+
99
+ const files = Array.from(fileRegistry.values());
100
+ fileTokens = files.reduce((acc, f) => acc + f.weight, 0);
101
+
102
+ return {
103
+ files: files.sort((a, b) => b.weight - a.weight).slice(0, 100),
104
+ totalTokens,
105
+ systemTokens: 0, // Pi provides this via ctx, not messages
106
+ historyTokens: totalTokens - fileTokens - toolTokens,
107
+ fileTokens,
108
+ toolTokens,
109
+ };
110
+ }
111
+
112
+ private extractPath(toolName: string, input: any): string | null {
113
+ if (toolName === "read" || toolName === "write" || toolName === "edit") {
114
+ return typeof input.path === "string" ? input.path : null;
115
+ }
116
+ if (toolName === "bash") {
117
+ // Simple regex for paths in bash commands (e.g., cat path/to/file)
118
+ const match = input.command?.match(
119
+ /(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
120
+ );
121
+ return match ? match[1] : null;
122
+ }
123
+ return null;
124
+ }
125
+
126
+ private getOpType(toolName: string): FileOp["type"] {
127
+ switch (toolName) {
128
+ case "write":
129
+ return "write";
130
+ case "edit":
131
+ return "edit";
132
+ case "bash":
133
+ return "delete"; // Simplified; usually bash implies modification or deletion
134
+ default:
135
+ return "read";
136
+ }
137
+ }
138
+
139
+ private calculateStatus(
140
+ turn: number,
141
+ currentTurn: number,
142
+ ): FileContext["status"] {
143
+ const diff = currentTurn - turn;
144
+ if (diff <= 3) return "active";
145
+ if (diff <= 10) return "stale";
146
+ return "legacy";
147
+ }
148
+
149
+ private findToolResult(
150
+ messages: any[],
151
+ toolTurnIndex: number,
152
+ toolId: string,
153
+ ): any {
154
+ // Look for the tool result immediately following the tool use
155
+ for (let i = toolTurnIndex + 1; i < messages.length; i++) {
156
+ if (messages[i].role === "tool" && messages[i].tool_call_id === toolId) {
157
+ return messages[i];
158
+ }
159
+ // If we hit another assistant turn, the result for this specific call is likely gone/compacted
160
+ if (messages[i].role === "assistant") break;
161
+ }
162
+ return null;
163
+ }
164
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * ReportGenerator
3
+ * Generates a visual HTML dashboard based on the ContextMap.
4
+ */
5
+
6
+ import type { ContextMap } from "./analyzer";
7
+ import { writeFileSync, mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+
11
+ export class ReportGenerator {
12
+ public static generateHTML(map: ContextMap): string {
13
+ const fileCards = map.files
14
+ .map(
15
+ (file) => `
16
+ <div class="file-card ${file.status}">
17
+ <div class="file-header">
18
+ <span class="file-path">${ReportGenerator.escapeHtml(file.path)}</span>
19
+ <span class="file-weight">${file.weight.toLocaleString()} tokens</span>
20
+ </div>
21
+ <div class="file-footer">
22
+ <span class="op-badge">${ReportGenerator.getOpIcon(file.lastOp.type)} ${file.lastOp.type}</span>
23
+ <span class="turn-badge">Turn ${file.lastOp.turn}</span>
24
+ <span class="status-text">${file.status.toUpperCase()}</span>
25
+ </div>
26
+ <div class="weight-bar">
27
+ <div class="weight-fill" style="width: ${Math.min(100, (file.weight / 1000) * 100)}%"></div>
28
+ </div>
29
+ </div>
30
+ `,
31
+ )
32
+ .join("");
33
+
34
+ const budgetPercent = (map.fileTokens / map.totalTokens) * 100;
35
+
36
+ return `
37
+ <!DOCTYPE html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="UTF-8">
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
+ <title>Pi Context Map</title>
43
+ <style>
44
+ :root {
45
+ --bg: #0f172a;
46
+ --card-bg: #1e293b;
47
+ --text: #f1f5f9;
48
+ --text-dim: #94a3b8;
49
+ --primary: #38bdf8;
50
+ --active: #22c55e;
51
+ --stale: #eab308;
52
+ --legacy: #ef4444;
53
+ --border: #334155;
54
+ }
55
+ body {
56
+ background: var(--bg);
57
+ color: var(--text);
58
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
59
+ margin: 0;
60
+ padding: 2rem;
61
+ line-height: 1.5;
62
+ }
63
+ .container { max-width: 1200px; margin: 0 auto; }
64
+ header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 2rem; }
65
+ h1 { font-size: 2rem; margin: 0; color: var(--primary); }
66
+ .stats-grid {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
69
+ gap: 1.5rem;
70
+ margin-top: 2rem;
71
+ }
72
+ .stat-card {
73
+ background: var(--card-bg);
74
+ padding: 1.5rem;
75
+ border-radius: 12px;
76
+ border: 1px solid var(--border);
77
+ text-align: center;
78
+ }
79
+ .stat-value { font-size: 1.5rem; font-weight: bold; display: block; }
80
+ .stat-label { color: var(--text-dim); font-size: 0.875rem; text-transform: uppercase; }
81
+
82
+ .budget-container {
83
+ margin: 2rem 0;
84
+ background: var(--card-bg);
85
+ padding: 1rem;
86
+ border-radius: 12px;
87
+ border: 1px solid var(--border);
88
+ }
89
+ .budget-bar {
90
+ height: 24px;
91
+ background: #020617;
92
+ border-radius: 12px;
93
+ display: flex;
94
+ overflow: hidden;
95
+ margin-bottom: 0.5rem;
96
+ }
97
+ .budget-segment { height: 100%; transition: width 0.3s ease; }
98
+ .seg-system { background: #6366f1; }
99
+ .seg-history { background: #a855f7; }
100
+ .seg-files { background: var(--primary); }
101
+ .seg-tools { background: #ec4899; }
102
+
103
+ .budget-legend {
104
+ display: flex;
105
+ gap: 1rem;
106
+ justify-content: center;
107
+ font-size: 0.75rem;
108
+ color: var(--text-dim);
109
+ }
110
+ .legend-item { display: flex; align-items: center; gap: 0.5rem; }
111
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
112
+
113
+ .file-grid {
114
+ display: grid;
115
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
116
+ gap: 1rem;
117
+ }
118
+ .file-card {
119
+ background: var(--card-bg);
120
+ border: 1px solid var(--border);
121
+ border-radius: 12px;
122
+ padding: 1rem;
123
+ transition: transform 0.2s ease, border-color 0.2s ease;
124
+ display: flex;
125
+ flex-direction: column;
126
+ justify-content: space-between;
127
+ }
128
+ .file-card:hover { transform: translateY(-4px); border-color: var(--primary); }
129
+ .file-header {
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: flex-start;
133
+ margin-bottom: 1rem;
134
+ }
135
+ .file-path {
136
+ font-family: 'Fira Code', monospace;
137
+ font-size: 0.875rem;
138
+ word-break: break-all;
139
+ margin-right: 1rem;
140
+ color: var(--text);
141
+ }
142
+ .file-weight { font-size: 0.75rem; color: var(--text-dim); white-space: nowrap; }
143
+ .file-footer {
144
+ display: flex;
145
+ justify-content: space-between;
146
+ align-items: center;
147
+ margin-top: 1rem;
148
+ font-size: 0.75rem;
149
+ }
150
+ .op-badge {
151
+ background: #0f172a;
152
+ padding: 2px 6px;
153
+ border-radius: 4px;
154
+ color: var(--text-dim);
155
+ }
156
+ .turn-badge { color: var(--text-dim); }
157
+ .status-text { font-weight: bold; text-transform: uppercase; }
158
+
159
+ /* Status Colors */
160
+ .active { border-left: 4px solid var(--active); }
161
+ .active .status-text { color: var(--active); }
162
+ .stale { border-left: 4px solid var(--stale); }
163
+ .stale .status-text { color: var(--stale); }
164
+ .legacy { border-left: 4px solid var(--legacy); }
165
+ .legacy .status-text { color: var(--legacy); }
166
+
167
+ .weight-bar {
168
+ height: 4px;
169
+ background: #020617;
170
+ border-radius: 2px;
171
+ margin-top: 1rem;
172
+ overflow: hidden;
173
+ }
174
+ .weight-fill {
175
+ height: 100%;
176
+ background: var(--primary);
177
+ transition: width 0.3s ease;
178
+ }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <div class="container">
183
+ <header>
184
+ <h1>Pi Context Map</h1>
185
+ <p style="color: var(--text-dim)">Session context window visualization and token distribution.</p>
186
+
187
+ <div class="stats-grid">
188
+ <div class="stat-card">
189
+ <span class="stat-value">${map.totalTokens.toLocaleString()}</span>
190
+ <span class="stat-label">Total Tokens</span>
191
+ </div>
192
+ <div class="stat-card">
193
+ <span class="stat-value">${map.files.length}</span>
194
+ <span class="stat-label">Files in Context</span>
195
+ </div>
196
+ <div class="stat-card">
197
+ <span class="stat-value">${map.fileTokens.toLocaleString()}</span>
198
+ <span class="stat-label">File Tokens</span>
199
+ </div>
200
+ <div class="stat-card">
201
+ <span class="stat-value">${Math.round(budgetPercent)}%</span>
202
+ <span class="stat-label">File Load</span>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="budget-container">
207
+ <div class="budget-bar">
208
+ <div class="budget-segment seg-system" style="width: ${(map.systemTokens / map.totalTokens) * 100 || 0}%"></div>
209
+ <div class="budget-segment seg-history" style="width: ${(map.historyTokens / map.totalTokens) * 100 || 0}%"></div>
210
+ <div class="budget-segment seg-files" style="width: ${(map.fileTokens / map.totalTokens) * 100 || 0}%"></div>
211
+ <div class="budget-segment seg-tools" style="width: ${(map.toolTokens / map.totalTokens) * 100 || 0}%"></div>
212
+ </div>
213
+ <div class="budget-legend">
214
+ <div class="legend-item"><span class="dot seg-system"></span> System</div>
215
+ <div class="legend-item"><span class="dot seg-history"></span> History</div>
216
+ <div class="legend-item"><span class="dot seg-files"></span> Files</div>
217
+ <div class="legend-item"><span class="dot seg-tools"></span> Tools</div>
218
+ </div>
219
+ </div>
220
+ </header>
221
+
222
+ <div class="file-grid">
223
+ ${fileCards}
224
+ </div>
225
+ </div>
226
+ </body>
227
+ </html>
228
+ `;
229
+ }
230
+
231
+ public static writeReport(html: string): string {
232
+ const reportDir = join(homedir(), ".pi", "context-map");
233
+ mkdirSync(reportDir, { recursive: true });
234
+ const reportPath = join(reportDir, "report.html");
235
+ writeFileSync(reportPath, html, "utf8");
236
+ return reportPath;
237
+ }
238
+
239
+ private static getOpIcon(type: string): string {
240
+ switch (type) {
241
+ case "read":
242
+ return "👁️";
243
+ case "write":
244
+ return "📝";
245
+ case "edit":
246
+ return "✍️";
247
+ case "delete":
248
+ return "🗑️";
249
+ default:
250
+ return "📄";
251
+ }
252
+ }
253
+
254
+ private static escapeHtml(text: string): string {
255
+ const map = {
256
+ "&": "&amp;",
257
+ "<": "&lt;",
258
+ ">": "&gt;",
259
+ '"': "&quot;",
260
+ "'": "&#039;",
261
+ };
262
+ return text.replace(/[&<>"']/g, (m) => map[m as keyof typeof map]);
263
+ }
264
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * pi-context-map
3
+ * Pi extension to visualize session context window and token distribution.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import { ContextAnalyzer } from "./analyzer";
8
+ import { ReportGenerator } from "./generator";
9
+
10
+ export default function (pi: ExtensionAPI) {
11
+ const analyzer = new ContextAnalyzer();
12
+
13
+ // Register the /context-map command
14
+ pi.registerCommand("context-map", {
15
+ description: "Generate a visual map of the current session context window.",
16
+ handler: (_args, ctx) => {
17
+ ctx.ui.notify("Analyzing session context...", "info");
18
+
19
+ try {
20
+ // 1. Extract messages and current turn
21
+ // Note: We assume ctx.session.messages is available.
22
+ // If not, we may need to fetch them via another API or use provided event data.
23
+ const messages = ctx.session.messages || [];
24
+ const currentTurn = messages.length;
25
+
26
+ if (messages.length === 0) {
27
+ ctx.ui.notify("No session history found to map.", "warning");
28
+ return;
29
+ }
30
+
31
+ // 2. Analyze context
32
+ const map = analyzer.analyze(messages, currentTurn);
33
+
34
+ // 3. Generate HTML Report
35
+ const html = ReportGenerator.generateHTML(map);
36
+ const reportPath = ReportGenerator.writeReport(html);
37
+
38
+ ctx.ui.notify(
39
+ `Context map generated successfully! \nPath: ${reportPath}`,
40
+ "success",
41
+ );
42
+
43
+ // Providing a link or instruction to open the report
44
+ ctx.ui.notify(
45
+ "You can open the report.html in your browser to see the visualization.",
46
+ "info",
47
+ );
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ ctx.ui.notify(`Failed to generate context map: ${message}`, "error");
51
+ }
52
+ },
53
+ });
54
+
55
+ // Optional: Notify the user when a significant amount of context is loaded
56
+ pi.on("session_before_compact", (event, ctx) => {
57
+ const { preparation } = event;
58
+ const tokens = preparation.tokensBefore;
59
+
60
+ if (tokens > 100_000) {
61
+ ctx.ui.notify(
62
+ `High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
63
+ "info",
64
+ );
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,16 @@
1
+ declare module "@earendil-works/pi-ai" {
2
+ export async function complete(
3
+ model: any,
4
+ params: {
5
+ messages: any[];
6
+ },
7
+ options?: {
8
+ apiKey?: string;
9
+ headers?: Record<string, string>;
10
+ maxTokens?: number;
11
+ signal?: AbortSignal;
12
+ }
13
+ ): Promise<{
14
+ content: Array<{ type: "text"; text: string }>;
15
+ }>;
16
+ }
@@ -0,0 +1,31 @@
1
+ declare module "@earendil-works/pi-coding-agent" {
2
+ export interface ExtensionAPI {
3
+ registerCommand(
4
+ name: string,
5
+ options: {
6
+ description: string;
7
+ handler: (
8
+ args: string | undefined,
9
+ ctx: ExtensionContext,
10
+ ) => Promise<void> | void;
11
+ },
12
+ ): void;
13
+ on(
14
+ event: string,
15
+ handler: (event: any, ctx: ExtensionContext) => Promise<void> | void,
16
+ ): void;
17
+ }
18
+
19
+ export interface ExtensionContext {
20
+ ui: {
21
+ notify(
22
+ message: string,
23
+ level: "info" | "success" | "warning" | "error",
24
+ ): void;
25
+ };
26
+ session: {
27
+ messages: any[];
28
+ };
29
+ modelRegistry: any;
30
+ }
31
+ }
package/tasks.md ADDED
@@ -0,0 +1,32 @@
1
+ # Implementation Tasks: pi-context-map
2
+
3
+ ## Phase 1: Foundation
4
+ - [ ] Initialize `package.json` and TypeScript config.
5
+ - [ ] Create project folder structure (`src/`, `docs/`).
6
+
7
+ ## Phase 2: Core Logic (`ContextAnalyzer`)
8
+ - [ ] Implement message scanning logic to find file operations.
9
+ - [ ] Implement token estimation heuristic.
10
+ - [ ] Implement status assignment (Active/Stale/Legacy).
11
+ - [ ] Create unit tests for analyzer logic.
12
+
13
+ ## Phase 3: Visualization (`ReportGenerator`)
14
+ - [ ] Design the HTML dashboard template.
15
+ - [ ] Implement data-to-HTML mapping.
16
+ - [ ] Implement file writing to `.pi/context-map/report.html`.
17
+
18
+ ## Phase 4: Pi Integration
19
+ - [ ] Register `/context-map` command.
20
+ - [ ] Implement command handler that triggers analysis $\to$ report $\to$ notification.
21
+ - [ ] Add "Open Report" link in the Pi notification.
22
+
23
+ ## Phase 5: QA & Polishing
24
+ - [ ] Test with high-token sessions.
25
+ - [ ] Verify accuracy of token weights.
26
+ - [ ] Run Pi Lens diagnostics to ensure zero blockers.
27
+ - [ ] Polish HTML CSS for "Architect" look.
28
+
29
+ ## Phase 6: Release
30
+ - [ ] Update README.md.
31
+ - [ ] Publish to npm.
32
+ - [ ] Final GitHub release.
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "*": ["src/types/*"]
16
+ }
17
+ },
18
+ "include": ["src/**/*"],
19
+ "typeRoots": ["./node_modules/@types", "src/types"]
20
+ }