pi-context 1.0.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 +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +215 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Pi Context Extension
|
|
2
|
+
|
|
3
|
+
A Git-like context management tool that allows AI agents to proactively manage their context.
|
|
4
|
+
|
|
5
|
+
Inspired by kimi-cli d-mail, implementing lossless time travel on the Pi session tree.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:pi-context
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### For Humans
|
|
16
|
+
|
|
17
|
+
Load the skill to enable the workflow:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
/skill:context-management
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
View detailed context window usage and token distribution with a visual dashboard. (like `claude code /context`)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
/context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
### For Agents
|
|
32
|
+
|
|
33
|
+
This extension adds the `context-management` skill with three core tools:
|
|
34
|
+
|
|
35
|
+
1. **🔖 Structure (`context_tag`)**
|
|
36
|
+
`git tag` Create named milestones to structure your conversation history.
|
|
37
|
+
|
|
38
|
+
2. **📊 Monitor (`context_log`)**
|
|
39
|
+
`git log` Visualize your conversation history, check token usage, and see where you are in the task tree.
|
|
40
|
+
|
|
41
|
+
3. **⏪ Compress (`context_checkout`)**
|
|
42
|
+
`git checkout` Move the HEAD pointer to any tag or commit ID. Compress completed tasks into a summary to free up context window space.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
4
|
+
const ContextLogParams = Type.Object({
|
|
5
|
+
limit: Type.Optional(Type.Number({ description: "History limit for visible entries (default: 50)." })),
|
|
6
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show ALL messages. If false, collapses intermediate steps." })),
|
|
7
|
+
});
|
|
8
|
+
const ContextCheckoutParams = Type.Object({
|
|
9
|
+
target: Type.String({ description: "Tag name or ID." }),
|
|
10
|
+
message: Type.String({ description: "Carryover message." }),
|
|
11
|
+
tagName: Type.Optional(Type.String()),
|
|
12
|
+
});
|
|
13
|
+
const ContextTagParams = Type.Object({
|
|
14
|
+
name: Type.String(),
|
|
15
|
+
target: Type.Optional(Type.String()),
|
|
16
|
+
});
|
|
17
|
+
const formatTokens = (n) => {
|
|
18
|
+
if (n >= 1_000_000)
|
|
19
|
+
return (n / 1_000_000).toFixed(1) + "M";
|
|
20
|
+
if (n >= 1_000)
|
|
21
|
+
return Math.round(n / 1_000) + "k";
|
|
22
|
+
return n.toString();
|
|
23
|
+
};
|
|
24
|
+
const resolveTargetId = (sm, target) => {
|
|
25
|
+
if (target.toLowerCase() === "root") {
|
|
26
|
+
const tree = sm.getTree();
|
|
27
|
+
return tree.length > 0 ? tree[0].entry.id : target;
|
|
28
|
+
}
|
|
29
|
+
if (/^[0-9a-f]{8,}$/i.test(target))
|
|
30
|
+
return target;
|
|
31
|
+
const find = (nodes) => {
|
|
32
|
+
for (const n of nodes) {
|
|
33
|
+
if (sm.getLabel(n.entry.id) === target)
|
|
34
|
+
return n.entry.id;
|
|
35
|
+
const r = find(n.children);
|
|
36
|
+
if (r)
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
return find(sm.getTree()) || target;
|
|
42
|
+
};
|
|
43
|
+
export default function (pi) {
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: "context_log",
|
|
46
|
+
label: "Context Log",
|
|
47
|
+
description: "Show history structure.",
|
|
48
|
+
parameters: ContextLogParams,
|
|
49
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
50
|
+
return { content: [{ type: "text", text: "Use /tree or context_log for history." }], details: {} };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
pi.registerTool({
|
|
54
|
+
name: "context_checkout",
|
|
55
|
+
label: "Context Checkout",
|
|
56
|
+
description: "Navigate history.",
|
|
57
|
+
parameters: ContextCheckoutParams,
|
|
58
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
59
|
+
const sm = ctx.sessionManager;
|
|
60
|
+
const tid = resolveTargetId(sm, params.target);
|
|
61
|
+
await sm.branchWithSummary(tid, params.message);
|
|
62
|
+
return { content: [{ type: "text", text: `Jumped to ${tid.slice(0, 7)}` }], details: {} };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: "context_tag",
|
|
67
|
+
label: "Context Tag",
|
|
68
|
+
description: "Create checkpoint.",
|
|
69
|
+
parameters: ContextTagParams,
|
|
70
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
71
|
+
const sm = ctx.sessionManager;
|
|
72
|
+
const id = params.target ? resolveTargetId(sm, params.target) : (sm.getLeafId() ?? "");
|
|
73
|
+
pi.setLabel(id, params.name);
|
|
74
|
+
return { content: [{ type: "text", text: `Tagged ${params.name}` }], details: {} };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
pi.registerCommand("context", {
|
|
78
|
+
description: "Show context usage visualization (Claude Code style)",
|
|
79
|
+
handler: async (args, ctx) => {
|
|
80
|
+
const usage = await ctx.getContextUsage();
|
|
81
|
+
if (!usage) {
|
|
82
|
+
ctx.ui.notify("Context usage info not available.", "warning");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const sm = ctx.sessionManager;
|
|
86
|
+
const branch = sm.getBranch();
|
|
87
|
+
const systemPrompt = ctx.getSystemPrompt();
|
|
88
|
+
const tools = pi.getActiveTools();
|
|
89
|
+
const allTools = pi.getAllTools();
|
|
90
|
+
const activeToolDefs = allTools.filter(t => tools.includes(t.name));
|
|
91
|
+
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
92
|
+
let msgTokensRaw = 0;
|
|
93
|
+
let toolUseTokensRaw = 0;
|
|
94
|
+
let toolResultTokensRaw = 0;
|
|
95
|
+
for (const entry of branch) {
|
|
96
|
+
if (entry.type === "message") {
|
|
97
|
+
const m = entry.message;
|
|
98
|
+
if (m.role === "user") {
|
|
99
|
+
if (typeof m.content === "string")
|
|
100
|
+
msgTokensRaw += estimateTokens(m.content);
|
|
101
|
+
else if (Array.isArray(m.content)) {
|
|
102
|
+
for (const p of m.content)
|
|
103
|
+
if (p.type === "text")
|
|
104
|
+
msgTokensRaw += estimateTokens(p.text);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (m.role === "assistant") {
|
|
108
|
+
if (typeof m.content === "string")
|
|
109
|
+
msgTokensRaw += estimateTokens(m.content);
|
|
110
|
+
else if (Array.isArray(m.content)) {
|
|
111
|
+
for (const p of m.content) {
|
|
112
|
+
if (p.type === "text")
|
|
113
|
+
msgTokensRaw += estimateTokens(p.text);
|
|
114
|
+
if (p.type === "toolCall")
|
|
115
|
+
toolUseTokensRaw += estimateTokens(JSON.stringify(p));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (m.role === "toolResult") {
|
|
120
|
+
if (Array.isArray(m.content)) {
|
|
121
|
+
for (const p of m.content)
|
|
122
|
+
if (p.type === "text")
|
|
123
|
+
toolResultTokensRaw += estimateTokens(p.text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (m.role === "bashExecution") {
|
|
127
|
+
toolUseTokensRaw += estimateTokens(m.command || "");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (entry.type === "branch_summary" || entry.type === "compaction") {
|
|
131
|
+
msgTokensRaw += estimateTokens(entry.summary || "");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const systemTokensRaw = estimateTokens(systemPrompt);
|
|
135
|
+
const toolDefTokensRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
136
|
+
const totalActual = usage.tokens;
|
|
137
|
+
const limit = usage.contextWindow;
|
|
138
|
+
const totalRaw = systemTokensRaw + toolDefTokensRaw + msgTokensRaw + toolUseTokensRaw + toolResultTokensRaw;
|
|
139
|
+
const ratio = totalRaw > 0 ? (totalActual / totalRaw) : 1;
|
|
140
|
+
const systemTokens = Math.round(systemTokensRaw * ratio);
|
|
141
|
+
const toolDefTokens = Math.round(toolDefTokensRaw * ratio);
|
|
142
|
+
const msgTokens = Math.round(msgTokensRaw * ratio);
|
|
143
|
+
const toolUseTokens = Math.round(toolUseTokensRaw * ratio);
|
|
144
|
+
const toolResultTokens = Math.round(toolResultTokensRaw * ratio);
|
|
145
|
+
await ctx.ui.custom((tui, theme, kb, done) => {
|
|
146
|
+
const container = new Container();
|
|
147
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
148
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Context Usage")), 1, 0));
|
|
149
|
+
container.addChild(new Spacer(1));
|
|
150
|
+
// Grouped by function and color
|
|
151
|
+
const categories = [
|
|
152
|
+
{ label: "System", value: systemTokens, color: "dim" },
|
|
153
|
+
{ label: "Tools", value: toolDefTokens, color: "dim" },
|
|
154
|
+
{ label: "Messages", value: msgTokens, color: "accent" },
|
|
155
|
+
{ label: "Tool Use", value: toolUseTokens, color: "success" },
|
|
156
|
+
{ label: "Tool Results", value: toolResultTokens, color: "success" },
|
|
157
|
+
];
|
|
158
|
+
const otherTokens = Math.max(0, totalActual - (systemTokens + toolDefTokens + msgTokens + toolUseTokens + toolResultTokens));
|
|
159
|
+
if (otherTokens > 10)
|
|
160
|
+
categories.push({ label: "Other", value: otherTokens, color: "dim" });
|
|
161
|
+
categories.push({ label: "Available", value: Math.max(0, limit - totalActual), color: "borderMuted" });
|
|
162
|
+
const gridWidth = 10;
|
|
163
|
+
const gridHeight = 5;
|
|
164
|
+
const totalBlocks = gridWidth * gridHeight;
|
|
165
|
+
const blocks = [];
|
|
166
|
+
categories.forEach((cat) => {
|
|
167
|
+
if (cat.label === "Available")
|
|
168
|
+
return;
|
|
169
|
+
let count = Math.round((cat.value / limit) * totalBlocks);
|
|
170
|
+
if (count === 0 && cat.value > 0)
|
|
171
|
+
count = 1;
|
|
172
|
+
for (let i = 0; i < count && blocks.length < totalBlocks; i++) {
|
|
173
|
+
blocks.push({ color: cat.color, filled: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
while (blocks.length < totalBlocks) {
|
|
177
|
+
blocks.push({ color: "borderMuted", filled: false });
|
|
178
|
+
}
|
|
179
|
+
const gridLines = [];
|
|
180
|
+
for (let r = 0; r < gridHeight; r++) {
|
|
181
|
+
let rowStr = "";
|
|
182
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
183
|
+
const b = blocks[r * gridWidth + c];
|
|
184
|
+
rowStr += theme.fg(b.color, b.filled ? "■ " : "□ ");
|
|
185
|
+
}
|
|
186
|
+
gridLines.push(rowStr.trimEnd());
|
|
187
|
+
}
|
|
188
|
+
const totalUsageTitle = `${theme.fg("text", theme.bold("Total Usage".padEnd(16)))} ${theme.fg("accent", theme.bold(formatTokens(totalActual).padStart(7)))} ${theme.fg("accent", theme.bold(`(${usage.percent.toFixed(1).padStart(5)}%)`))}`;
|
|
189
|
+
const catDetailLines = categories.map(cat => {
|
|
190
|
+
const labelStr = cat.label.padEnd(14);
|
|
191
|
+
const valStr = formatTokens(cat.value).padStart(7);
|
|
192
|
+
const rowPercent = ((cat.value / limit) * 100).toFixed(1).padStart(5);
|
|
193
|
+
const icon = cat.label === "Available" ? "□" : "■";
|
|
194
|
+
return `${theme.fg(cat.color, icon)} ${theme.fg("text", labelStr)} ${theme.fg("accent", valStr)} (${rowPercent}%)`;
|
|
195
|
+
});
|
|
196
|
+
const allDetailLines = [totalUsageTitle, "", ...catDetailLines];
|
|
197
|
+
const leftSideWidth = 20;
|
|
198
|
+
const maxH = Math.max(gridLines.length, allDetailLines.length);
|
|
199
|
+
for (let i = 0; i < maxH; i++) {
|
|
200
|
+
const left = (gridLines[i] || "").padEnd(leftSideWidth);
|
|
201
|
+
const right = allDetailLines[i] || "";
|
|
202
|
+
container.addChild(new Text(` ${left} ${right}`, 1, 0));
|
|
203
|
+
}
|
|
204
|
+
container.addChild(new Spacer(1));
|
|
205
|
+
container.addChild(new Text(theme.fg("dim", " Press any key to close"), 1, 0));
|
|
206
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
207
|
+
return {
|
|
208
|
+
render: (w) => container.render(w),
|
|
209
|
+
invalidate: () => container.invalidate(),
|
|
210
|
+
handleInput: (data) => done(undefined),
|
|
211
|
+
};
|
|
212
|
+
}, { overlay: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-context",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "agent-driven context management tools",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi-agent",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"context-management",
|
|
15
|
+
"context-optimization",
|
|
16
|
+
"time-travel",
|
|
17
|
+
"checkpoint"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@sinclair/typebox": "^0.34.13"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.7.2",
|
|
30
|
+
"@mariozechner/pi-coding-agent": "latest"
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./dist/index.js"
|
|
35
|
+
],
|
|
36
|
+
"skills": [
|
|
37
|
+
"./skills/context-management/SKILL.md"
|
|
38
|
+
],
|
|
39
|
+
"image": "https://github.com/ttttmr/pi-context/raw/refs/heads/main/img/context.png"
|
|
40
|
+
}
|
|
41
|
+
}
|