@ryan_nookpi/pi-extension-headroom 0.1.0 → 0.1.1
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 +23 -14
- package/config.ts +61 -15
- package/index.ts +27 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,17 +43,26 @@ The footer shows a compact status (`✓ Headroom -42% (12,345 saved)`) once comp
|
|
|
43
43
|
|
|
44
44
|
## Configuration
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
Settings are read at startup from `~/.pi/agent/headroom/settings.json`. Values in this file override environment variables; environment variables remain supported as fallbacks.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"minContextTokens": 10000,
|
|
53
|
+
"minMessageChars": 1000
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Setting key | Env fallback | Default | Description |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `enabled` | `PI_HEADROOM_ENABLED` | `true` | Enable compression on start. |
|
|
60
|
+
| `baseUrl` (`url` also accepted) | `PI_HEADROOM_URL` (`HEADROOM_URL` / `HEADROOM_BASE_URL` also accepted) | `http://127.0.0.1:8788` | Proxy base URL. |
|
|
61
|
+
| `allowRemote` | `PI_HEADROOM_ALLOW_REMOTE` | `false` | Allow non-local proxy URLs. |
|
|
62
|
+
| `autoStart` | `PI_HEADROOM_AUTO_START` | `true` | Auto-start a local persistent proxy when offline. |
|
|
63
|
+
| `command` | `PI_HEADROOM_COMMAND` | `headroom` | Command used to launch the proxy. |
|
|
64
|
+
| `minContextTokens` | `PI_HEADROOM_MIN_CONTEXT_TOKENS` | `20000` | Skip compression below this context token count. |
|
|
65
|
+
| `minMessageChars` | `PI_HEADROOM_MIN_MESSAGE_CHARS` | `2000` | Only compress tool results at or above this size. |
|
|
66
|
+
| `timeoutMs` | `PI_HEADROOM_TIMEOUT_MS` | `15000` | HTTP timeout for proxy requests. |
|
|
67
|
+
|
|
68
|
+
Boolean values accept JSON booleans, or strings such as `1/0`, `true/false`, `yes/no`, `on/off`.
|
package/config.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
1
4
|
import type { HeadroomConfig } from "./types.ts";
|
|
2
5
|
|
|
3
6
|
const DEFAULT_BASE_URL = "http://127.0.0.1:8788";
|
|
@@ -5,19 +8,55 @@ const DEFAULT_MIN_CONTEXT_TOKENS = 20_000;
|
|
|
5
8
|
const DEFAULT_MIN_MESSAGE_CHARS = 2_000;
|
|
6
9
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
7
10
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
export const HEADROOM_SETTINGS_DIR = path.join(os.homedir(), ".pi", "agent", "headroom");
|
|
12
|
+
export const HEADROOM_SETTINGS_FILE = path.join(HEADROOM_SETTINGS_DIR, "settings.json");
|
|
13
|
+
|
|
14
|
+
export interface HeadroomSettings {
|
|
15
|
+
enabled?: boolean | string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
allowRemote?: boolean | string;
|
|
19
|
+
autoStart?: boolean | string;
|
|
20
|
+
command?: string;
|
|
21
|
+
minContextTokens?: number | string;
|
|
22
|
+
minMessageChars?: number | string;
|
|
23
|
+
timeoutMs?: number | string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadHeadroomSettings(settingsPath: string = HEADROOM_SETTINGS_FILE): HeadroomSettings {
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
29
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
30
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as HeadroomSettings;
|
|
31
|
+
} catch {
|
|
32
|
+
// Missing or invalid settings.json falls back to env/defaults.
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadHeadroomConfig(
|
|
38
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
39
|
+
settings: HeadroomSettings = env === process.env ? loadHeadroomSettings() : {},
|
|
40
|
+
): HeadroomConfig {
|
|
41
|
+
const envBaseUrl = env.PI_HEADROOM_URL || env.HEADROOM_URL || env.HEADROOM_BASE_URL || DEFAULT_BASE_URL;
|
|
42
|
+
const baseUrl = normalizeBaseUrl(parseString(settings.baseUrl ?? settings.url, envBaseUrl));
|
|
12
43
|
return {
|
|
13
|
-
enabled: parseBoolean(env.PI_HEADROOM_ENABLED, true),
|
|
44
|
+
enabled: parseBoolean(settings.enabled, parseBoolean(env.PI_HEADROOM_ENABLED, true)),
|
|
14
45
|
baseUrl,
|
|
15
|
-
allowRemote: parseBoolean(env.PI_HEADROOM_ALLOW_REMOTE, false),
|
|
16
|
-
autoStart: parseBoolean(env.PI_HEADROOM_AUTO_START, true),
|
|
17
|
-
command: env.PI_HEADROOM_COMMAND?.trim() || "headroom",
|
|
18
|
-
minContextTokens: parseInteger(
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
allowRemote: parseBoolean(settings.allowRemote, parseBoolean(env.PI_HEADROOM_ALLOW_REMOTE, false)),
|
|
47
|
+
autoStart: parseBoolean(settings.autoStart, parseBoolean(env.PI_HEADROOM_AUTO_START, true)),
|
|
48
|
+
command: parseString(settings.command, env.PI_HEADROOM_COMMAND?.trim() || "headroom"),
|
|
49
|
+
minContextTokens: parseInteger(
|
|
50
|
+
settings.minContextTokens,
|
|
51
|
+
parseInteger(env.PI_HEADROOM_MIN_CONTEXT_TOKENS, DEFAULT_MIN_CONTEXT_TOKENS, 0),
|
|
52
|
+
0,
|
|
53
|
+
),
|
|
54
|
+
minMessageChars: parseInteger(
|
|
55
|
+
settings.minMessageChars,
|
|
56
|
+
parseInteger(env.PI_HEADROOM_MIN_MESSAGE_CHARS, DEFAULT_MIN_MESSAGE_CHARS, 1),
|
|
57
|
+
1,
|
|
58
|
+
),
|
|
59
|
+
timeoutMs: parseInteger(settings.timeoutMs, parseInteger(env.PI_HEADROOM_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 100), 100),
|
|
21
60
|
};
|
|
22
61
|
}
|
|
23
62
|
|
|
@@ -39,17 +78,24 @@ function normalizeBaseUrl(raw: string): string {
|
|
|
39
78
|
return trimmed.replace(/\/+$/, "");
|
|
40
79
|
}
|
|
41
80
|
|
|
42
|
-
function
|
|
81
|
+
function parseString(raw: unknown, fallback: string): string {
|
|
82
|
+
if (typeof raw !== "string") return fallback;
|
|
83
|
+
return raw.trim() || fallback;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseBoolean(raw: unknown, fallback: boolean): boolean {
|
|
43
87
|
if (raw === undefined) return fallback;
|
|
88
|
+
if (typeof raw === "boolean") return raw;
|
|
89
|
+
if (typeof raw !== "string") return fallback;
|
|
44
90
|
const normalized = raw.trim().toLowerCase();
|
|
45
91
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
46
92
|
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
47
93
|
return fallback;
|
|
48
94
|
}
|
|
49
95
|
|
|
50
|
-
function parseInteger(raw:
|
|
96
|
+
function parseInteger(raw: unknown, fallback: number, min: number): number {
|
|
51
97
|
if (raw === undefined) return fallback;
|
|
52
|
-
const parsed = Number.parseInt(raw, 10);
|
|
98
|
+
const parsed = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN;
|
|
53
99
|
if (!Number.isFinite(parsed) || parsed < min) return fallback;
|
|
54
|
-
return parsed;
|
|
100
|
+
return Math.trunc(parsed);
|
|
55
101
|
}
|
package/index.ts
CHANGED
|
@@ -303,19 +303,33 @@ function refreshStatus(ctx: ExtensionContext, config: HeadroomConfig, state: Hea
|
|
|
303
303
|
ctx.ui.setStatus(STATUS_KEY, renderFooterStatus(ctx, config, state));
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
type HeadroomStatusColor = "dim" | "warning" | "success";
|
|
307
|
+
|
|
308
|
+
type HeadroomStatusTheme = {
|
|
309
|
+
fg(color: HeadroomStatusColor, text: string): string;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
function isHeadroomStatusTheme(theme: unknown): theme is HeadroomStatusTheme {
|
|
313
|
+
return typeof (theme as { fg?: unknown } | null)?.fg === "function";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createStatusPainter(theme: unknown): (color: HeadroomStatusColor, text: string) => string {
|
|
317
|
+
if (isHeadroomStatusTheme(theme)) return (color, text) => theme.fg(color, text);
|
|
318
|
+
return (_color, text) => text;
|
|
319
|
+
}
|
|
320
|
+
|
|
306
321
|
function renderFooterStatus(ctx: ExtensionContext, config: HeadroomConfig, state: HeadroomRuntimeState): string {
|
|
307
|
-
const
|
|
308
|
-
if (!state.enabled) return
|
|
309
|
-
if (isRemoteBlocked(config)) return
|
|
310
|
-
if (state.proxyStarting) return
|
|
311
|
-
if (state.proxyOnline === false) return
|
|
312
|
-
if (state.proxyOnline === null && !state.stats.last) return
|
|
313
|
-
if (!state.stats.last) return
|
|
322
|
+
const paint = createStatusPainter(ctx.ui.theme);
|
|
323
|
+
if (!state.enabled) return paint("dim", "○ Headroom off");
|
|
324
|
+
if (isRemoteBlocked(config)) return paint("warning", "⚠") + paint("dim", " Headroom remote blocked");
|
|
325
|
+
if (state.proxyStarting) return paint("dim", "⏳ Headroom starting");
|
|
326
|
+
if (state.proxyOnline === false) return paint("dim", "○ Headroom not running");
|
|
327
|
+
if (state.proxyOnline === null && !state.stats.last) return paint("dim", "○ Headroom idle");
|
|
328
|
+
if (!state.stats.last) return paint("success", "✓") + paint("dim", " Headroom");
|
|
314
329
|
|
|
315
330
|
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
316
331
|
return (
|
|
317
|
-
|
|
318
|
-
theme.fg("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`)
|
|
332
|
+
paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`)
|
|
319
333
|
);
|
|
320
334
|
}
|
|
321
335
|
|
|
@@ -414,3 +428,7 @@ function parseSubcommand(args: string): Subcommand {
|
|
|
414
428
|
const normalized = args.trim().toLowerCase();
|
|
415
429
|
return SUBCOMMANDS.includes(normalized as Subcommand) ? (normalized as Subcommand) : "status";
|
|
416
430
|
}
|
|
431
|
+
|
|
432
|
+
export const __test__ = {
|
|
433
|
+
renderFooterStatus,
|
|
434
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryan_nookpi/pi-extension-headroom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Headroom token compression for pi — compress large tool results via a local Headroom proxy to reclaim context window.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|