pi-context-map 0.1.4 → 0.3.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/CHANGELOG.md +19 -22
- package/{src → extensions}/analyzer.ts +78 -50
- package/{src → extensions}/generator.ts +100 -49
- package/extensions/index.ts +89 -0
- package/extensions/insights.ts +114 -0
- package/extensions/token-counter.ts +149 -0
- package/extensions/types/pi-coding-agent.d.ts +3 -0
- package/package.json +21 -17
- package/dist/analyzer.d.ts +0 -40
- package/dist/analyzer.js +0 -117
- package/dist/generator.d.ts +0 -11
- package/dist/generator.js +0 -258
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -50
- package/docs/proposal.md +0 -41
- package/docs/spec.md +0 -57
- package/src/index.ts +0 -67
- package/src/types/pi-coding-agent.d.ts +0 -31
- package/tasks.md +0 -32
- package/tsconfig.json +0 -20
- /package/{src → extensions}/types/pi-ai.d.ts +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenCounter
|
|
3
|
+
* Code-aware token estimation. Avoids heavy external tokenizers (e.g., tiktoken)
|
|
4
|
+
* by applying multipliers based on content structure.
|
|
5
|
+
*
|
|
6
|
+
* Heuristic base: 4 characters per token (the standard for English text).
|
|
7
|
+
* Adjustments:
|
|
8
|
+
* - Code blocks (```...```) are denser in tokens (identifiers, symbols).
|
|
9
|
+
* - JSON payloads have more structural overhead.
|
|
10
|
+
* - Pure whitespace is under-weighted.
|
|
11
|
+
*/
|
|
12
|
+
export class TokenCounter {
|
|
13
|
+
/** Base heuristic: average English is ~4 characters per token. */
|
|
14
|
+
private static BASE_CHARS_PER_TOKEN = 4;
|
|
15
|
+
|
|
16
|
+
/** Multiplier for fenced code blocks (```` ``` ````). */
|
|
17
|
+
private static CODE_MULTIPLIER = 1.3;
|
|
18
|
+
|
|
19
|
+
/** Multiplier for JSON-like structures. */
|
|
20
|
+
private static JSON_MULTIPLIER = 1.5;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Count estimated tokens for a raw string of text.
|
|
24
|
+
*/
|
|
25
|
+
public static count(text: string): number {
|
|
26
|
+
if (!text) return 0;
|
|
27
|
+
|
|
28
|
+
let total = 0;
|
|
29
|
+
let cursor = 0;
|
|
30
|
+
const len = text.length;
|
|
31
|
+
|
|
32
|
+
while (cursor < len) {
|
|
33
|
+
// Detect fenced code blocks
|
|
34
|
+
const fenceStart = text.indexOf("```", cursor);
|
|
35
|
+
if (fenceStart !== -1) {
|
|
36
|
+
// Count everything up to the fence as regular text
|
|
37
|
+
total += TokenCounter.regularChunk(text.substring(cursor, fenceStart));
|
|
38
|
+
// Find the closing fence
|
|
39
|
+
const fenceEnd = text.indexOf("```", fenceStart + 3);
|
|
40
|
+
if (fenceEnd === -1) {
|
|
41
|
+
// Unclosed fence — treat the rest as code
|
|
42
|
+
total += TokenCounter.codeChunk(text.substring(fenceStart));
|
|
43
|
+
cursor = len;
|
|
44
|
+
} else {
|
|
45
|
+
total += TokenCounter.codeChunk(
|
|
46
|
+
text.substring(fenceStart, fenceEnd + 3),
|
|
47
|
+
);
|
|
48
|
+
cursor = fenceEnd + 3;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect JSON-like content (starts with { or [ and has balanced structure)
|
|
54
|
+
const trimmed = text.substring(cursor).trimStart();
|
|
55
|
+
const firstChar = trimmed.charAt(0);
|
|
56
|
+
if (
|
|
57
|
+
(firstChar === "{" || firstChar === "[") &&
|
|
58
|
+
TokenCounter.looksLikeJson(trimmed)
|
|
59
|
+
) {
|
|
60
|
+
const jsonLen = TokenCounter.extractJsonLength(trimmed);
|
|
61
|
+
if (jsonLen > 0) {
|
|
62
|
+
total += TokenCounter.jsonChunk(trimmed.substring(0, jsonLen));
|
|
63
|
+
cursor += text.substring(cursor).indexOf(trimmed) + jsonLen;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default: regular text
|
|
69
|
+
total += TokenCounter.regularChunk(text.substring(cursor));
|
|
70
|
+
cursor = len;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Math.ceil(total);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convenience: count tokens for any message shape Pi uses.
|
|
78
|
+
*/
|
|
79
|
+
public static countMessage(msg: any): number {
|
|
80
|
+
if (!msg) return 0;
|
|
81
|
+
if (typeof msg.content === "string") {
|
|
82
|
+
return TokenCounter.count(msg.content);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(msg.content)) {
|
|
85
|
+
return msg.content.reduce((sum: number, block: any) => {
|
|
86
|
+
if (typeof block === "string") return sum + TokenCounter.count(block);
|
|
87
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
88
|
+
return sum + TokenCounter.count(block.text);
|
|
89
|
+
}
|
|
90
|
+
if (block.type === "tool_use" || block.type === "tool_result") {
|
|
91
|
+
return sum + TokenCounter.count(JSON.stringify(block));
|
|
92
|
+
}
|
|
93
|
+
return sum + TokenCounter.count(JSON.stringify(block));
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
return TokenCounter.count(JSON.stringify(msg));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static regularChunk(text: string): number {
|
|
100
|
+
return text.length / TokenCounter.BASE_CHARS_PER_TOKEN;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static codeChunk(text: string): number {
|
|
104
|
+
return (
|
|
105
|
+
(text.length / TokenCounter.BASE_CHARS_PER_TOKEN) *
|
|
106
|
+
TokenCounter.CODE_MULTIPLIER
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static jsonChunk(text: string): number {
|
|
111
|
+
return (
|
|
112
|
+
(text.length / TokenCounter.BASE_CHARS_PER_TOKEN) *
|
|
113
|
+
TokenCounter.JSON_MULTIPLIER
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static looksLikeJson(text: string): boolean {
|
|
118
|
+
// Quick heuristic: contains quoted keys and structural punctuation
|
|
119
|
+
return /"[^"]+"\s*:/i.test(text) && /[{}[\],]/.test(text);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static extractJsonLength(text: string): number {
|
|
123
|
+
let depth = 0;
|
|
124
|
+
let inString = false;
|
|
125
|
+
let escape = false;
|
|
126
|
+
for (let i = 0; i < text.length; i++) {
|
|
127
|
+
const ch = text[i];
|
|
128
|
+
if (escape) {
|
|
129
|
+
escape = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === "\\") {
|
|
133
|
+
escape = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === '"') {
|
|
137
|
+
inString = !inString;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inString) continue;
|
|
141
|
+
if (ch === "{" || ch === "[") depth++;
|
|
142
|
+
else if (ch === "}" || ch === "]") {
|
|
143
|
+
depth--;
|
|
144
|
+
if (depth === 0) return i + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"dev": "tsc -w"
|
|
10
|
-
},
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Professional context profiler for Pi that visualizes the session context window, token distribution, and integrates with Nexus packages for actionable insights.",
|
|
11
5
|
"keywords": [
|
|
12
|
-
"pi",
|
|
13
6
|
"pi-package",
|
|
14
|
-
"extension",
|
|
15
7
|
"context",
|
|
16
8
|
"visualization",
|
|
17
|
-
"tokens"
|
|
9
|
+
"tokens",
|
|
10
|
+
"profiler",
|
|
11
|
+
"nexus"
|
|
18
12
|
],
|
|
19
|
-
"author": "ZachDreamZ",
|
|
20
|
-
"license": "MIT",
|
|
21
13
|
"pi": {
|
|
22
|
-
"extensions": ["
|
|
14
|
+
"extensions": ["./extensions"]
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"extensions",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
23
26
|
},
|
|
24
|
-
"dependencies": {},
|
|
25
27
|
"peerDependencies": {
|
|
26
|
-
"
|
|
27
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
28
|
+
"pi-coding-agent": "*"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"typescript": "^5.0.0",
|
|
32
|
+
"jest": "^29.0.0",
|
|
33
|
+
"ts-jest": "^29.0.0",
|
|
34
|
+
"@types/jest": "^29.0.0",
|
|
31
35
|
"@types/node": "^20.0.0"
|
|
32
36
|
}
|
|
33
37
|
}
|
package/dist/analyzer.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/analyzer.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
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;
|
package/dist/generator.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/generator.js
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
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
|
-
"&": "&",
|
|
250
|
-
"<": "<",
|
|
251
|
-
">": ">",
|
|
252
|
-
'"': """,
|
|
253
|
-
"'": "'",
|
|
254
|
-
};
|
|
255
|
-
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
exports.ReportGenerator = ReportGenerator;
|