pi-agent-extensions 0.3.5 → 0.4.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
CHANGED
|
@@ -34,6 +34,8 @@ Original repository: https://github.com/mitsuhiko/agent-stuff
|
|
|
34
34
|
| **cwd_history** | Tracker | Tracks directory changes in context | ✅ Stable |
|
|
35
35
|
| **btw** | Command | Quick side questions without history | ✅ Stable |
|
|
36
36
|
| **nvidia-nim** | Command | Nvidia NIM auth & config | ✅ Stable |
|
|
37
|
+
| **powerline-footer** | UI | Custom powerline-style footer bar | ✅ Stable |
|
|
38
|
+
| **session-breakdown** | Command | Session analytics dashboard | ✅ Stable |
|
|
37
39
|
|
|
38
40
|
## Install
|
|
39
41
|
|
|
@@ -290,7 +292,7 @@ Ask quick "by the way" side questions without polluting your conversation histor
|
|
|
290
292
|
- ✅ Ephemeral overlay — nothing enters session history
|
|
291
293
|
- ✅ Zero context cost — no tokens wasted
|
|
292
294
|
- ✅ Scrollable answer (↑↓/j/k, PgUp/PgDn)
|
|
293
|
-
- ✅
|
|
295
|
+
- ✅ Uses your currently selected model
|
|
294
296
|
- ✅ Non-UI fallback (prints to stdout)
|
|
295
297
|
|
|
296
298
|
**When to use `/btw` vs normal prompts:**
|
|
@@ -338,6 +340,23 @@ Authenticate and configure Nvidia NIM as an LLM provider.
|
|
|
338
340
|
- Adds configured models to `~/.pi/agent/settings.json` `enabledModels` for scoped `/model` + Ctrl+P cycling
|
|
339
341
|
- Model IDs must be `org/model` (exactly one `/`), e.g. `moonshotai/kimi-k2.5` (not `nvidia/moonshotai/kimi-k2.5`)
|
|
340
342
|
|
|
343
|
+
**Powerline Footer**
|
|
344
|
+
Custom powerline-style footer replacing the default pi footer with richer information.
|
|
345
|
+
- Git branch + working tree status (staged/unstaged/untracked/ahead/behind)
|
|
346
|
+
- Model name + context usage with color-coded percentage
|
|
347
|
+
- Estimated session cost from model pricing
|
|
348
|
+
- Session duration timer
|
|
349
|
+
- Python virtualenv / conda environment detection
|
|
350
|
+
- Extension statuses + session name
|
|
351
|
+
- Auto-refreshes every 10 seconds via async git commands
|
|
352
|
+
|
|
353
|
+
**Session Breakdown (`/session-breakdown`)**
|
|
354
|
+
Interactive analytics dashboard for your pi sessions.
|
|
355
|
+
- Analyzes all sessions in `~/.pi/agent/sessions/`
|
|
356
|
+
- Shows sessions/day, messages/day, tokens/day, cost/day
|
|
357
|
+
- Model breakdown with per-model usage stats
|
|
358
|
+
- Filterable by 7/30/90 day windows
|
|
359
|
+
|
|
341
360
|
## Development
|
|
342
361
|
|
|
343
362
|
```bash
|
package/extensions/btw/index.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - Zero context cost — no tokens wasted on history
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { complete, type
|
|
20
|
+
import { complete, type UserMessage } from "@mariozechner/pi-ai";
|
|
21
21
|
import type {
|
|
22
22
|
ExtensionAPI,
|
|
23
23
|
ExtensionCommandContext,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type TUI,
|
|
39
39
|
} from "@mariozechner/pi-tui";
|
|
40
40
|
|
|
41
|
-
import { getRequestAuth
|
|
41
|
+
import { getRequestAuth } from "../shared/auth.js";
|
|
42
42
|
import {
|
|
43
43
|
BTW_SYSTEM_PROMPT,
|
|
44
44
|
buildBtwUserMessage,
|
|
@@ -46,40 +46,6 @@ import {
|
|
|
46
46
|
extractResponseText,
|
|
47
47
|
} from "./btw.js";
|
|
48
48
|
|
|
49
|
-
const HAIKU_MODEL_ID = "claude-haiku-4-5";
|
|
50
|
-
const CODEX_MODEL_ID = "gpt-5.1-codex-mini";
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Select a cheap/fast model for BTW side questions.
|
|
54
|
-
* Prefers Codex mini → Haiku → current model.
|
|
55
|
-
*/
|
|
56
|
-
async function selectBtwModel(
|
|
57
|
-
currentModel: Model<Api>,
|
|
58
|
-
modelRegistry: {
|
|
59
|
-
find: (provider: string, modelId: string) => Model<Api> | undefined;
|
|
60
|
-
getApiKeyAndHeaders: (
|
|
61
|
-
model: Model<Api>,
|
|
62
|
-
) => Promise<
|
|
63
|
-
| { ok: true; apiKey?: string; headers?: Record<string, string> }
|
|
64
|
-
| { ok: false; error: string }
|
|
65
|
-
>;
|
|
66
|
-
},
|
|
67
|
-
): Promise<Model<Api>> {
|
|
68
|
-
// Try Codex mini first (cheapest)
|
|
69
|
-
const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
|
|
70
|
-
if (codexModel && (await hasRequestAuth(modelRegistry, codexModel))) {
|
|
71
|
-
return codexModel;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Try Haiku (fast & cheap)
|
|
75
|
-
const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
|
|
76
|
-
if (haikuModel && (await hasRequestAuth(modelRegistry, haikuModel))) {
|
|
77
|
-
return haikuModel;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Fallback to current model
|
|
81
|
-
return currentModel;
|
|
82
|
-
}
|
|
83
49
|
|
|
84
50
|
/**
|
|
85
51
|
* Overlay component that displays the BTW answer.
|
|
@@ -299,8 +265,8 @@ async function runBtwCommand(
|
|
|
299
265
|
conversationText = serializeConversation(llmMessages);
|
|
300
266
|
}
|
|
301
267
|
|
|
302
|
-
//
|
|
303
|
-
const btwModel =
|
|
268
|
+
// Use the currently selected model
|
|
269
|
+
const btwModel = ctx.model;
|
|
304
270
|
|
|
305
271
|
// Build LLM messages
|
|
306
272
|
const userMessage: UserMessage = {
|
|
@@ -189,11 +189,11 @@ function getSocketPath(sessionId: string): string {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function isSafeSessionId(sessionId: string): boolean {
|
|
192
|
-
return
|
|
192
|
+
return /^[a-zA-Z0-9_-]+$/.test(sessionId);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function isSafeAlias(alias: string): boolean {
|
|
196
|
-
return
|
|
196
|
+
return /^[a-zA-Z0-9_ -]+$/.test(alias);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
function getAliasPath(alias: string): string {
|
|
@@ -15,8 +15,12 @@ import { Markdown, type MarkdownTheme } from "@mariozechner/pi-tui";
|
|
|
15
15
|
* Send a desktop notification via OSC 777 escape sequence.
|
|
16
16
|
*/
|
|
17
17
|
const notify = (title: string, body: string): void => {
|
|
18
|
+
// Sanitize to prevent terminal escape sequence injection (remove control characters like ESC and BEL)
|
|
19
|
+
const safeTitle = title.replace(/[\x00-\x1f\x7f]/g, "");
|
|
20
|
+
const safeBody = body.replace(/[\x00-\x1f\x7f]/g, "");
|
|
21
|
+
|
|
18
22
|
// OSC 777 format: ESC ] 777 ; notify ; title ; body BEL
|
|
19
|
-
process.stdout.write(`\x1b]777;notify;${
|
|
23
|
+
process.stdout.write(`\x1b]777;notify;${safeTitle};${safeBody}\x07`);
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
const isTextPart = (part: unknown): part is { type: "text"; text: string } =>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ContextUsage, ReadonlyFooterDataProvider } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { type Component, type Theme, type TUI, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import * as child_process from "child_process";
|
|
4
|
+
|
|
5
|
+
class PowerlineFooter implements Component {
|
|
6
|
+
private tui: TUI;
|
|
7
|
+
private theme: Theme;
|
|
8
|
+
private footerData: ReadonlyFooterDataProvider;
|
|
9
|
+
private ctx: ExtensionContext;
|
|
10
|
+
private interval?: ReturnType<typeof setInterval>;
|
|
11
|
+
private sessionStartTime: number;
|
|
12
|
+
|
|
13
|
+
// Cached git state (updated async every 10s)
|
|
14
|
+
private gitStatusExtras: string = "";
|
|
15
|
+
|
|
16
|
+
constructor(tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider, ctx: ExtensionContext) {
|
|
17
|
+
this.tui = tui;
|
|
18
|
+
this.theme = theme;
|
|
19
|
+
this.footerData = footerData;
|
|
20
|
+
this.ctx = ctx;
|
|
21
|
+
this.sessionStartTime = Date.now();
|
|
22
|
+
|
|
23
|
+
this.fetchAsyncData();
|
|
24
|
+
|
|
25
|
+
this.interval = setInterval(() => {
|
|
26
|
+
this.fetchAsyncData();
|
|
27
|
+
this.tui.requestRender();
|
|
28
|
+
}, 10000);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dispose() {
|
|
32
|
+
if (this.interval) {
|
|
33
|
+
clearInterval(this.interval);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private fetchAsyncData() {
|
|
38
|
+
const cwd = this.ctx.cwd;
|
|
39
|
+
const branch = this.footerData.getGitBranch();
|
|
40
|
+
|
|
41
|
+
if (branch) {
|
|
42
|
+
child_process.exec("git --no-optional-locks status --porcelain", { encoding: "utf8", cwd }, (err, stdout) => {
|
|
43
|
+
if (err) return;
|
|
44
|
+
const staged = (stdout.match(/^[AMDRC]/gm) || []).length;
|
|
45
|
+
const unstaged = (stdout.match(/^.[MD]/gm) || []).length;
|
|
46
|
+
const untracked = (stdout.match(/^\?\?/gm) || []).length;
|
|
47
|
+
|
|
48
|
+
child_process.exec("git --no-optional-locks rev-list --count --left-right @{u}...HEAD", { encoding: "utf8", cwd }, (err2, revListOut) => {
|
|
49
|
+
let ahead = 0;
|
|
50
|
+
let behind = 0;
|
|
51
|
+
if (!err2 && revListOut && revListOut.includes("\t")) {
|
|
52
|
+
const [b, a] = revListOut.trim().split("\t");
|
|
53
|
+
behind = parseInt(b, 10);
|
|
54
|
+
ahead = parseInt(a, 10);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let statusExtras = "";
|
|
58
|
+
if (staged > 0) statusExtras += `+${staged}`;
|
|
59
|
+
if (unstaged > 0) statusExtras += `!${unstaged}`;
|
|
60
|
+
if (untracked > 0) statusExtras += `?${untracked}`;
|
|
61
|
+
if (ahead > 0) statusExtras += `⇡${ahead}`;
|
|
62
|
+
if (behind > 0) statusExtras += `⇣${behind}`;
|
|
63
|
+
this.gitStatusExtras = statusExtras;
|
|
64
|
+
this.tui.requestRender();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
this.gitStatusExtras = "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
invalidate() {}
|
|
73
|
+
handleInput(_data: string) {}
|
|
74
|
+
|
|
75
|
+
private formatTokens(num: number): string {
|
|
76
|
+
if (num >= 1_000_000) {
|
|
77
|
+
return (num / 1_000_000).toFixed(1) + "M";
|
|
78
|
+
}
|
|
79
|
+
if (num >= 1000) {
|
|
80
|
+
return Math.floor(num / 1000) + "K";
|
|
81
|
+
}
|
|
82
|
+
return String(num);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(width: number): string[] {
|
|
86
|
+
const RESET = "\x1b[0m";
|
|
87
|
+
const DIM = "\x1b[2m";
|
|
88
|
+
const BOLD = "\x1b[1m";
|
|
89
|
+
const MAUVE = "\x1b[38;5;183m";
|
|
90
|
+
const BLUE = "\x1b[38;5;111m";
|
|
91
|
+
const GREEN = "\x1b[38;5;150m";
|
|
92
|
+
const YELLOW = "\x1b[38;5;222m";
|
|
93
|
+
const RED = "\x1b[38;5;211m";
|
|
94
|
+
const SKY = "\x1b[38;5;117m";
|
|
95
|
+
const PEACH = "\x1b[38;5;216m";
|
|
96
|
+
const OVERLAY2 = "\x1b[38;5;103m";
|
|
97
|
+
|
|
98
|
+
// --- Directory ---
|
|
99
|
+
const cwd = this.ctx.cwd;
|
|
100
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
101
|
+
let shortDir = cwd;
|
|
102
|
+
if (homeDir && shortDir.startsWith(homeDir)) {
|
|
103
|
+
shortDir = "~" + shortDir.slice(homeDir.length);
|
|
104
|
+
}
|
|
105
|
+
const parts = shortDir.split("/");
|
|
106
|
+
if (parts.length > 4) {
|
|
107
|
+
shortDir = parts.slice(parts.length - 4).join("/");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Git ---
|
|
111
|
+
let gitInfo = "";
|
|
112
|
+
const branch = this.footerData.getGitBranch();
|
|
113
|
+
if (branch) {
|
|
114
|
+
gitInfo = ` ${DIM}on${RESET} ${MAUVE} ${branch}${RESET}`;
|
|
115
|
+
if (this.gitStatusExtras) {
|
|
116
|
+
gitInfo += ` ${RED}${this.gitStatusExtras}${RESET}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Model ---
|
|
121
|
+
const model = this.ctx.model;
|
|
122
|
+
const modelShort = model?.name || model?.id || "unknown";
|
|
123
|
+
|
|
124
|
+
// --- Context usage ---
|
|
125
|
+
const contextUsage = this.ctx.getContextUsage();
|
|
126
|
+
let contextInfo = "";
|
|
127
|
+
let costInfo = "";
|
|
128
|
+
|
|
129
|
+
if (contextUsage) {
|
|
130
|
+
const used = contextUsage.tokens;
|
|
131
|
+
const total = contextUsage.contextWindow;
|
|
132
|
+
const pct = contextUsage.percent;
|
|
133
|
+
const remaining = 100 - pct;
|
|
134
|
+
|
|
135
|
+
let ctxColor = GREEN;
|
|
136
|
+
if (remaining < 20) {
|
|
137
|
+
ctxColor = RED;
|
|
138
|
+
} else if (remaining < 50) {
|
|
139
|
+
ctxColor = YELLOW;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
contextInfo = `${ctxColor}${this.formatTokens(used)}/${this.formatTokens(total)} (${pct}%)${RESET}`;
|
|
143
|
+
|
|
144
|
+
// Cost estimation from model pricing
|
|
145
|
+
if (model?.cost && contextUsage.usageTokens > 0) {
|
|
146
|
+
// Rough estimate: treat usageTokens as input, trailingTokens as recent output
|
|
147
|
+
const inputTokens = contextUsage.usageTokens;
|
|
148
|
+
const outputTokens = contextUsage.trailingTokens;
|
|
149
|
+
const cost =
|
|
150
|
+
(inputTokens * model.cost.input) / 1_000_000 +
|
|
151
|
+
(outputTokens * model.cost.output) / 1_000_000;
|
|
152
|
+
|
|
153
|
+
if (cost > 0.001) {
|
|
154
|
+
costInfo = ` ${DIM}|${RESET} ${PEACH}$${cost.toFixed(2)}${RESET}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Session duration ---
|
|
160
|
+
let durationInfo = "";
|
|
161
|
+
const durationMs = Date.now() - this.sessionStartTime;
|
|
162
|
+
if (durationMs > 0) {
|
|
163
|
+
const durS = Math.floor(durationMs / 1000);
|
|
164
|
+
let durFmt = "";
|
|
165
|
+
if (durS >= 3600) {
|
|
166
|
+
const durH = Math.floor(durS / 3600);
|
|
167
|
+
const durM = Math.floor((durS % 3600) / 60);
|
|
168
|
+
durFmt = `${durH}h${durM}m`;
|
|
169
|
+
} else if (durS >= 60) {
|
|
170
|
+
durFmt = `${Math.floor(durS / 60)}m`;
|
|
171
|
+
} else {
|
|
172
|
+
durFmt = `${durS}s`;
|
|
173
|
+
}
|
|
174
|
+
durationInfo = ` ${DIM}|${RESET} ${OVERLAY2}${durFmt}${RESET}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Python env ---
|
|
178
|
+
let envInfo = "";
|
|
179
|
+
if (process.env.CONDA_DEFAULT_ENV) {
|
|
180
|
+
envInfo = ` ${SKY}(${process.env.CONDA_DEFAULT_ENV})${RESET}`;
|
|
181
|
+
} else if (process.env.VIRTUAL_ENV) {
|
|
182
|
+
const venvName = process.env.VIRTUAL_ENV.split("/").pop() || "";
|
|
183
|
+
envInfo = ` ${SKY}(${venvName})${RESET}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Time ---
|
|
187
|
+
const date = new Date();
|
|
188
|
+
const currentTime = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
189
|
+
|
|
190
|
+
// --- Session name ---
|
|
191
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
192
|
+
const sessionInfo = sessionName ? `${MAUVE}[${sessionName}]${RESET} ` : "";
|
|
193
|
+
|
|
194
|
+
// --- Extension statuses ---
|
|
195
|
+
let statusInfo = "";
|
|
196
|
+
const statuses = this.footerData.getExtensionStatuses();
|
|
197
|
+
if (statuses.size > 0) {
|
|
198
|
+
const parts: string[] = [];
|
|
199
|
+
for (const [, text] of statuses) {
|
|
200
|
+
if (text) parts.push(text);
|
|
201
|
+
}
|
|
202
|
+
if (parts.length > 0) {
|
|
203
|
+
statusInfo = ` ${DIM}|${RESET} ${parts.join(" ")}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const line = `${sessionInfo}${BOLD}${BLUE} ${shortDir}${RESET}${gitInfo} ${DIM}|${RESET} ${OVERLAY2}${modelShort}${RESET} ${contextInfo}${costInfo}${durationInfo}${envInfo}${statusInfo} ${DIM}${currentTime}${RESET}`;
|
|
208
|
+
|
|
209
|
+
return [line];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default function (pi: ExtensionAPI) {
|
|
214
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
215
|
+
if (!ctx.hasUI) return;
|
|
216
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
217
|
+
return new PowerlineFooter(tui, theme, footerData, ctx);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Collection of extensions for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"./extensions/todos/index.ts",
|
|
48
48
|
"./extensions/whimsical/index.ts",
|
|
49
49
|
"./extensions/nvidia-nim/index.ts",
|
|
50
|
-
"./extensions/btw/index.ts"
|
|
50
|
+
"./extensions/btw/index.ts",
|
|
51
|
+
"./extensions/powerline-footer/index.ts"
|
|
51
52
|
],
|
|
52
53
|
"themes": [
|
|
53
54
|
"./themes/nightowl.json",
|