pi-plus 0.1.2 → 0.1.3

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
@@ -12,8 +12,8 @@ at CC's auto-compact threshold.
12
12
 
13
13
  | Behavior | CC source | Here |
14
14
  |---|---|---|
15
- | Context window resolution (env override → `[1m]` suffix → capability ≥100k → 200k default) | `utils/context.ts` | `src/model-config.ts` |
16
- | Max output tokens per model + 8k slot-reservation cap + env override | `utils/context.ts`, `services/api/claude.ts` | `src/model-config.ts` |
15
+ | Context window resolution (`[1m]` suffix → capability ≥100k → 200k default) | `utils/context.ts` | `src/model-config.ts` |
16
+ | Max output tokens per model + 8k slot-reservation cap | `utils/context.ts`, `services/api/claude.ts` | `src/model-config.ts` |
17
17
  | Effective window = window − min(maxOutput, 20k) | `services/compact/autoCompact.ts` | `src/model-config.ts` |
18
18
  | Auto-compact threshold = effective − 13,000 | `services/compact/autoCompact.ts` | `src/model-config.ts` |
19
19
  | Context measurement: last API usage (input+cacheWrite+cacheRead+output) + chars/4 tail estimate; images/PDFs flat 2000 | `utils/tokens.ts`, `services/tokenEstimation.ts` | `src/usage.ts` |
@@ -21,6 +21,12 @@ at CC's auto-compact threshold.
21
21
  | Warning/error/blocking thresholds (20k/20k/3k buffers) | `services/compact/autoCompact.ts` | `src/percentages.ts` |
22
22
  | Indicator text: dim `N% until auto-compact`, `Context low (N% remaining) · Run /compact to compact & continue` | `components/TokenWarning.tsx` | `src/indicator.ts` |
23
23
 
24
+ This plugin supports **no Claude Code environment variables**
25
+ (`CLAUDE_CODE_*`, `DISABLE_COMPACT`, … are all ignored). All configuration is
26
+ done through the settings system described below; the only env vars honored
27
+ are the plugin's own kill-switches, `PI_PLUS_NO_AUTO_COMPACT` and
28
+ `PI_PLUS_NO_LOG`.
29
+
24
30
  On a real session the plugin's token count matches pi's own `ctx.getContextUsage().tokens`
25
31
  exactly for the Anthropic-style usage shape (pi's `cacheWrite` already includes the 1h
26
32
  cache subset and `reasoning` ⊂ `output`, so the field-for-field sums coincide).
@@ -55,28 +61,58 @@ auto-compact and CC's autocompact use). The trigger is edge-triggered: it
55
61
  fires once per crossing and re-arms after post-compact usage drops below the
56
62
  threshold. Manual `/compact` keeps working.
57
63
 
58
- Set `PI_CC_CONTEXT_NO_AUTO_COMPACT=1` to disable the plugin's trigger (indicator only).
64
+ Set `PI_PLUS_NO_AUTO_COMPACT=1` to disable the plugin's trigger (indicator only).
59
65
 
60
- ## Environment variables
66
+ ## Configuration
61
67
 
62
- All CC variables are honored with CC's semantics:
68
+ Settings are read through [pi-extension-settings](https://github.com/juanibiapina/pi-extension-settings)
69
+ (the pi ecosystem's standard settings helpers — `getSetting()`), stored under a
70
+ `pi-plus` section. Global values live in `~/.pi/agent/settings-extensions.json`,
71
+ project values in `.pi/settings-extensions.json` (project wins per key). Values
72
+ are strings; invalid or absent values fall back to the hardcoded CC-derived
73
+ defaults. If `@juanibiapina/pi-extension-settings` is installed, the settings
74
+ also appear in its `/extension-settings` UI.
63
75
 
64
- | Variable | Effect |
65
- |---|---|
66
- | `CLAUDE_CODE_MAX_CONTEXT_TOKENS` | Hard context-window override (highest precedence) |
67
- | `CLAUDE_CODE_DISABLE_1M_CONTEXT` | Clamp windows >200k to 200k; ignore `[1m]` |
68
- | `CLAUDE_CODE_AUTO_COMPACT_WINDOW` | Cap the window before threshold math |
69
- | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Override max output tokens (bounded by model's limit) |
70
- | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | Auto-compact threshold as % of effective window (testing) |
71
- | `CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE` | Override the blocking limit |
72
- | `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` | CC-style auto-compact off (switches indicator to "Context low … Run /compact") |
76
+ ```json
77
+ {
78
+ "pi-plus": {
79
+ "autoCompact": "true",
80
+ "contextWindow": "256000",
81
+ "autoCompactThreshold": "235000",
82
+ "autoCompactPercent": "50",
83
+ "autoCompactWindowCap": "200000",
84
+ "maxOutputTokens": "8192",
85
+ "disableOutputTokenCap": "false",
86
+ "blockingLimit": "190000"
87
+ }
88
+ }
89
+ ```
73
90
 
74
- Plugin-specific:
91
+ | Setting | Effect | Default |
92
+ |---|---|---|
93
+ | `autoCompact` | Master switch for the plugin's auto-compact trigger (`true`/`false`) | `true` |
94
+ | `contextWindow` | Override the model-derived context window (tokens) | model default (200k) |
95
+ | `autoCompactThreshold` | Absolute token threshold for auto-compact; wins over everything below | effective window − 13k |
96
+ | `autoCompactPercent` | Threshold as % of effective window (1–100, for testing) | off |
97
+ | `autoCompactWindowCap` | Cap the window before threshold math (tokens) | off |
98
+ | `maxOutputTokens` | Override max output tokens (bounded by the model's limit) | 8k-capped model default |
99
+ | `disableOutputTokenCap` | Use the model's native default max output instead of the 8k slot cap | `false` |
100
+ | `blockingLimit` | Absolute token usage that blocks further work | effective window − 3k |
101
+
102
+ ## Usage-detection log
103
+
104
+ Every usage check is appended as one JSON line to `pi-plus.log` inside pi's
105
+ agent directory (`$PI_CODING_AGENT_DIR` or `~/.pi/agent`). Each line records
106
+ the event (`usage_check` from the indicator or the auto-compact trigger,
107
+ plus `auto_compact_fired` / `auto_compact_error`), the model, the measured
108
+ token count, and the threshold/percent state:
109
+
110
+ ```json
111
+ {"ts":"2026-09-17T13:06:11.672Z","event":"usage_check","source":"indicator","model":"kimi-k2","tokens":9977,"percentLeft":94,"aboveWarning":false,"aboveError":false,"aboveAutoCompact":false,"atBlockingLimit":false}
112
+ ```
75
113
 
76
- | Variable | Effect |
77
- |---|---|
78
- | `PI_CC_CONTEXT_DISABLE_OTK_CAP` | Disable the 8k slot-reservation cap on max output tokens |
79
- | `PI_CC_CONTEXT_NO_AUTO_COMPACT` | Never call `ctx.compact()` from the plugin |
114
+ Logging is best-effort — a write failure never affects the host agent.
115
+ Set `PI_PLUS_NO_LOG=1` to disable it.
80
116
 
81
117
  ## Known limits
82
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plus",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Claude Code-exact context usage detection for pi (extension)",
5
5
  "type": "module",
6
6
  "pi": {
@@ -44,5 +44,8 @@
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-ai": "*",
46
46
  "@earendil-works/pi-coding-agent": "*"
47
+ },
48
+ "dependencies": {
49
+ "@juanibiapina/pi-extension-settings": "^0.10.0"
47
50
  }
48
51
  }
package/src/index.ts CHANGED
@@ -13,10 +13,13 @@
13
13
  */
14
14
 
15
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import type { SettingDefinition } from "@juanibiapina/pi-extension-settings";
16
17
  import { initialTriggerState, shouldFireAutoCompact, type AutoCompactTriggerState } from "./auto-compact.ts";
17
18
  import { getAutoCompactThreshold } from "./model-config.ts";
18
19
  import { isAutoCompactEnabled, calculateTokenWarningState } from "./percentages.ts";
19
20
  import { renderTokenWarning } from "./indicator.ts";
21
+ import { appendUsageLog } from "./logger.ts";
22
+ import { loadPiPlusSettings } from "./settings.ts";
20
23
  import { tokenCountWithEstimation, type RoughMessage } from "./usage.ts";
21
24
 
22
25
  const STATUS_KEY = "cc-context";
@@ -29,20 +32,90 @@ function branchMessages(ctx: ExtensionContext): RoughMessage[] {
29
32
  }
30
33
 
31
34
  export default function (pi: ExtensionAPI) {
35
+ // Register our settings so they appear in the /extension-settings UI
36
+ // (provided by @juanibiapina/pi-extension-settings when installed).
37
+ pi.events.emit("pi-extension-settings:register", {
38
+ name: "pi-plus",
39
+ settings: [
40
+ {
41
+ id: "autoCompact",
42
+ label: "Auto-compact at threshold",
43
+ description: "Call /compact automatically when usage crosses the auto-compact threshold",
44
+ defaultValue: "true",
45
+ values: ["true", "false"],
46
+ },
47
+ {
48
+ id: "contextWindow",
49
+ label: "Context window override",
50
+ description: "Override the model-derived context window (tokens). Empty = model default.",
51
+ defaultValue: "",
52
+ },
53
+ {
54
+ id: "autoCompactThreshold",
55
+ label: "Auto-compact threshold",
56
+ description: "Absolute token threshold for auto-compact. Empty = effective window − 13k.",
57
+ defaultValue: "",
58
+ },
59
+ {
60
+ id: "autoCompactPercent",
61
+ label: "Auto-compact percent",
62
+ description: "Threshold as % of effective window (1–100, testing). Empty = off.",
63
+ defaultValue: "",
64
+ },
65
+ {
66
+ id: "autoCompactWindowCap",
67
+ label: "Auto-compact window cap",
68
+ description: "Cap the window before threshold math (tokens). Empty = off.",
69
+ defaultValue: "",
70
+ },
71
+ {
72
+ id: "maxOutputTokens",
73
+ label: "Max output tokens",
74
+ description: "Override max output tokens (bounded by the model's limit). Empty = default.",
75
+ defaultValue: "",
76
+ },
77
+ {
78
+ id: "disableOutputTokenCap",
79
+ label: "Disable 8k output cap",
80
+ description: "Use the model's native default max output instead of the 8k slot cap",
81
+ defaultValue: "false",
82
+ values: ["false", "true"],
83
+ },
84
+ {
85
+ id: "blockingLimit",
86
+ label: "Blocking limit",
87
+ description: "Absolute token usage that blocks further work. Empty = effective window − 3k.",
88
+ defaultValue: "",
89
+ },
90
+ ] satisfies SettingDefinition[],
91
+ });
92
+
32
93
  // Edge-trigger state for the CC-threshold auto-compact. Reset per session;
33
94
  // re-arms automatically once post-compact usage drops below the threshold.
34
95
  let compactTrigger: AutoCompactTriggerState = initialTriggerState();
35
96
 
36
97
  function updateIndicator(ctx: ExtensionContext): void {
37
- if (!ctx.hasUI) return;
38
98
  const model = ctx.model;
39
99
  if (!model) {
40
- ctx.ui.setStatus(STATUS_KEY, undefined);
100
+ if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
41
101
  return;
42
102
  }
103
+ const settings = loadPiPlusSettings();
43
104
  const tokenCount = tokenCountWithEstimation(branchMessages(ctx));
44
- const state = calculateTokenWarningState(tokenCount, model.id, model.contextWindow, model.maxTokens);
45
- ctx.ui.setStatus(STATUS_KEY, renderTokenWarning(state, isAutoCompactEnabled(), ctx.ui.theme));
105
+ const state = calculateTokenWarningState(tokenCount, model.id, model.contextWindow, model.maxTokens, settings);
106
+ appendUsageLog({
107
+ event: "usage_check",
108
+ source: "indicator",
109
+ model: model.id,
110
+ tokens: tokenCount,
111
+ percentLeft: state.percentLeft,
112
+ aboveWarning: state.isAboveWarningThreshold,
113
+ aboveError: state.isAboveErrorThreshold,
114
+ aboveAutoCompact: state.isAboveAutoCompactThreshold,
115
+ atBlockingLimit: state.isAtBlockingLimit,
116
+ });
117
+ if (!ctx.hasUI) return;
118
+ ctx.ui.setStatus(STATUS_KEY, renderTokenWarning(state, isAutoCompactEnabled(settings), ctx.ui.theme));
46
119
  }
47
120
 
48
121
  /**
@@ -51,22 +124,35 @@ export default function (pi: ExtensionAPI) {
51
124
  * (tool_execution_end) — pi performs the compaction between agent steps
52
125
  * and continues the turn with the compacted context.
53
126
  *
54
- * Set PI_CC_CONTEXT_NO_AUTO_COMPACT=1 to disable, and disable pi's
127
+ * Set PI_PLUS_NO_AUTO_COMPACT=1 to disable, and disable pi's
55
128
  * built-in auto-compaction (settings.json "compaction": {"enabled": false})
56
129
  * so this threshold governs instead of pi's window − 16,384.
57
130
  */
58
131
  function maybeAutoCompact(ctx: ExtensionContext): void {
59
132
  const model = ctx.model;
60
133
  if (!model) return;
61
- if (!isAutoCompactEnabled()) return;
62
- if (process.env.PI_CC_CONTEXT_NO_AUTO_COMPACT !== undefined) return;
134
+ const settings = loadPiPlusSettings();
135
+ if (!isAutoCompactEnabled(settings)) return;
136
+ if (process.env.PI_PLUS_NO_AUTO_COMPACT !== undefined) return;
63
137
  const tokenCount = tokenCountWithEstimation(branchMessages(ctx));
64
- const threshold = getAutoCompactThreshold(model.id, model.contextWindow, model.maxTokens);
65
- if (!shouldFireAutoCompact(compactTrigger, tokenCount, threshold)) return;
138
+ const threshold = getAutoCompactThreshold(model.id, model.contextWindow, model.maxTokens, settings);
139
+ const fired = shouldFireAutoCompact(compactTrigger, tokenCount, threshold);
140
+ appendUsageLog({
141
+ event: "usage_check",
142
+ source: "auto_compact",
143
+ model: model.id,
144
+ tokens: tokenCount,
145
+ threshold,
146
+ belowThreshold: tokenCount < threshold,
147
+ fired,
148
+ });
149
+ if (!fired) return;
66
150
  try {
67
151
  ctx.compact();
152
+ appendUsageLog({ event: "auto_compact_fired", model: model.id, tokens: tokenCount, threshold });
68
153
  } catch (error) {
69
154
  ctx.ui.notify?.(`pi-plus: auto-compact failed to start: ${String(error)}`, "warning");
155
+ appendUsageLog({ event: "auto_compact_error", model: model.id, error: String(error) });
70
156
  }
71
157
  }
72
158
 
package/src/logger.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Usage-detection logging to a log file inside pi's agent directory
3
+ * (`$PI_CODING_AGENT_DIR` or `~/.pi/agent`, same resolution as pi's
4
+ * getAgentDir() in coding-agent config.ts).
5
+ *
6
+ * pi itself has no general runtime agent log (only an on-demand debug log
7
+ * and a TUI crash log), so this plugin appends its own `pi-plus.log` there.
8
+ * Lines are JSON — one object per usage check — so they can be inspected or
9
+ * replayed easily.
10
+ *
11
+ * Logging is strictly best-effort: any filesystem error is swallowed so the
12
+ * plugin can never crash or disturb the host agent.
13
+ */
14
+
15
+ import { appendFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ /** Mirrors pi's ENV_AGENT_DIR (`PI_CODING_AGENT_DIR` for the stock build). */
20
+ const ENV_AGENT_DIR = "PI_CODING_AGENT_DIR";
21
+
22
+ /** Set PI_PLUS_NO_LOG=1 to disable file logging. */
23
+ export function isLogEnabled(): boolean {
24
+ const v = process.env.PI_PLUS_NO_LOG;
25
+ return v === undefined || v === "" || v === "0" || v.toLowerCase() === "false";
26
+ }
27
+
28
+ function expandTildePath(p: string): string {
29
+ if (p === "~") return homedir();
30
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
31
+ return p;
32
+ }
33
+
34
+ export function getAgentDir(): string {
35
+ const envDir = process.env[ENV_AGENT_DIR];
36
+ return envDir ? expandTildePath(envDir) : join(homedir(), ".pi", "agent");
37
+ }
38
+
39
+ export function getPiPlusLogPath(): string {
40
+ return join(getAgentDir(), "pi-plus.log");
41
+ }
42
+
43
+ export type UsageLogFields = Record<string, string | number | boolean | null | undefined>;
44
+
45
+ export function formatUsageLogLine(fields: UsageLogFields): string {
46
+ return JSON.stringify({ ts: new Date().toISOString(), ...fields }) + "\n";
47
+ }
48
+
49
+ export function appendUsageLog(fields: UsageLogFields): void {
50
+ if (!isLogEnabled()) return;
51
+ try {
52
+ appendFileSync(getPiPlusLogPath(), formatUsageLogLine(fields));
53
+ } catch {
54
+ // Best-effort: never propagate logging failures into the host agent.
55
+ }
56
+ }
@@ -5,6 +5,10 @@
5
5
  * - src/utils/context.ts — getContextWindowForModel, getModelMaxOutputTokens
6
6
  * - src/services/api/claude.ts — getMaxOutputTokensForModel (slot-reservation cap)
7
7
  * - src/services/compact/autoCompact.ts — getEffectiveContextWindowSize, getAutoCompactThreshold
8
+ *
9
+ * This plugin intentionally supports NO environment variables (CC's
10
+ * CLAUDE_CODE_* env overrides included). Everything user-configurable lives
11
+ * in the `pi-plus` section of pi's settings.json — see settings.ts.
8
12
  */
9
13
 
10
14
  export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000;
@@ -17,41 +21,37 @@ const MAX_OUTPUT_TOKENS_UPPER_LIMIT = 64_000;
17
21
 
18
22
  const ONE_MILLION = 1_000_000;
19
23
 
20
- function isEnvTruthy(value: string | undefined): boolean {
21
- return value !== undefined && value !== "" && value !== "0" && value.toLowerCase() !== "false";
22
- }
23
-
24
- function parsePositiveInt(raw: string | undefined): number | undefined {
25
- if (raw === undefined || raw === "") return undefined;
26
- const parsed = parseInt(raw, 10);
27
- return !Number.isNaN(parsed) && parsed > 0 ? parsed : undefined;
28
- }
29
-
30
- export function is1mContextDisabled(): boolean {
31
- return isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT);
24
+ /** The `pi-plus` settings.json section, as it affects model config. */
25
+ export interface ModelConfigSettings {
26
+ /** Override the model-derived context window (tokens). */
27
+ contextWindow?: number;
28
+ /** Cap the window before threshold math (CC: CLAUDE_CODE_AUTO_COMPACT_WINDOW). */
29
+ autoCompactWindowCap?: number;
30
+ /** Override max output tokens, bounded by the model's upper limit. */
31
+ maxOutputTokens?: number;
32
+ /** Disable the 8k slot-reservation cap on default max output tokens. */
33
+ disableOutputTokenCap?: boolean;
34
+ /** Override the auto-compact threshold directly (absolute tokens). */
35
+ autoCompactThreshold?: number;
36
+ /** Threshold as a percent (1–100) of the effective window (testing). */
37
+ autoCompactPercent?: number;
32
38
  }
33
39
 
34
40
  /** CC: has1mContext — explicit `[1m]` model suffix opt-in. */
35
41
  export function has1mContext(modelId: string): boolean {
36
- return !is1mContextDisabled() && /\[1m\]/i.test(modelId);
42
+ return /\[1m\]/i.test(modelId);
37
43
  }
38
44
 
39
45
  /**
40
46
  * CC: getContextWindowForModel.
41
- * Resolution order: CLAUDE_CODE_MAX_CONTEXT_TOKENS env override → `[1m]` suffix →
42
- * model capability (pi's registry `contextWindow` plays the role of CC's
43
- * `max_input_tokens` capability, with the same >= 100k gate) → 200k default.
47
+ * Resolution order: `[1m]` suffix → model capability (pi's registry
48
+ * `contextWindow` plays the role of CC's `max_input_tokens` capability,
49
+ * with the same >= 100k gate) → 200k default.
44
50
  */
45
51
  export function resolveContextWindow(modelId: string, modelContextWindow?: number): number {
46
- const override = parsePositiveInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS);
47
- if (override) return override;
48
-
49
52
  if (has1mContext(modelId)) return ONE_MILLION;
50
53
 
51
54
  if (modelContextWindow && modelContextWindow >= 100_000) {
52
- if (modelContextWindow > MODEL_CONTEXT_WINDOW_DEFAULT && is1mContextDisabled()) {
53
- return MODEL_CONTEXT_WINDOW_DEFAULT;
54
- }
55
55
  return modelContextWindow;
56
56
  }
57
57
  return MODEL_CONTEXT_WINDOW_DEFAULT;
@@ -111,21 +111,23 @@ export function getModelMaxOutputTokens(
111
111
 
112
112
  /**
113
113
  * CC: getMaxOutputTokensForModel — applies the slot-reservation cap
114
- * (tengu_otk_slot_v1, live in current production builds: default output
115
- * dropped to 8k, over-limit requests get one clean retry at 64k), then the
116
- * CLAUDE_CODE_MAX_OUTPUT_TOKENS bounded env override.
114
+ * (tengu_otk_slot_v1: default output dropped to 8k), unless disabled in
115
+ * settings, then the `maxOutputTokens` bounded settings override.
117
116
  */
118
- export function getMaxOutputTokensForModel(modelId: string, modelMaxTokens?: number): number {
117
+ export function getMaxOutputTokensForModel(
118
+ modelId: string,
119
+ modelMaxTokens?: number,
120
+ settings?: ModelConfigSettings,
121
+ ): number {
119
122
  const { default: modelDefault, upperLimit } = getModelMaxOutputTokens(modelId, modelMaxTokens);
120
- const capDisabled = isEnvTruthy(process.env.PI_CC_CONTEXT_DISABLE_OTK_CAP);
121
123
  // Math.min keeps models with lower native defaults (e.g. claude-3-opus at
122
124
  // 4k) at their native value.
123
- const defaultTokens = capDisabled ? modelDefault : Math.min(modelDefault, CAPPED_DEFAULT_MAX_TOKENS);
125
+ const defaultTokens = settings?.disableOutputTokenCap
126
+ ? modelDefault
127
+ : Math.min(modelDefault, CAPPED_DEFAULT_MAX_TOKENS);
124
128
 
125
- const raw = process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS;
126
- if (raw !== undefined) {
127
- const parsed = parseInt(raw, 10);
128
- if (!Number.isNaN(parsed) && parsed > 0) return Math.min(parsed, upperLimit);
129
+ if (settings?.maxOutputTokens !== undefined && settings.maxOutputTokens > 0) {
130
+ return Math.min(settings.maxOutputTokens, upperLimit);
129
131
  }
130
132
  return defaultTokens;
131
133
  }
@@ -133,44 +135,50 @@ export function getMaxOutputTokensForModel(modelId: string, modelMaxTokens?: num
133
135
  /**
134
136
  * CC: getEffectiveContextWindowSize — context window minus the reserved
135
137
  * output tokens (min(maxOutput, 20k)), optionally capped by
136
- * CLAUDE_CODE_AUTO_COMPACT_WINDOW.
138
+ * `autoCompactWindowCap` from settings.
139
+ *
140
+ * `settings.contextWindow` replaces the model-derived window first.
137
141
  */
138
142
  export function getEffectiveContextWindowSize(
139
143
  modelId: string,
140
144
  modelContextWindow?: number,
141
145
  modelMaxTokens?: number,
146
+ settings?: ModelConfigSettings,
142
147
  ): number {
143
148
  const reservedTokensForSummary = Math.min(
144
- getMaxOutputTokensForModel(modelId, modelMaxTokens),
149
+ getMaxOutputTokensForModel(modelId, modelMaxTokens, settings),
145
150
  COMPACT_MAX_OUTPUT_TOKENS,
146
151
  );
147
- let contextWindow = resolveContextWindow(modelId, modelContextWindow);
148
- const autoCompactWindow = parsePositiveInt(process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW);
149
- if (autoCompactWindow) {
150
- contextWindow = Math.min(contextWindow, autoCompactWindow);
152
+ let contextWindow = settings?.contextWindow ?? resolveContextWindow(modelId, modelContextWindow);
153
+ const windowCap = settings?.autoCompactWindowCap;
154
+ if (windowCap !== undefined && windowCap > 0) {
155
+ contextWindow = Math.min(contextWindow, windowCap);
151
156
  }
152
157
  return contextWindow - reservedTokensForSummary;
153
158
  }
154
159
 
155
160
  /**
156
161
  * CC: getAutoCompactThreshold — effective window minus the 13k autocompact
157
- * buffer. CLAUDE_AUTOCOMPACT_PCT_OVERRIDE (1–100) lowers it for testing.
162
+ * buffer. `autoCompactPercent` (1–100) lowers it for testing.
163
+ *
164
+ * `settings.autoCompactThreshold` (absolute tokens) wins outright;
165
+ * otherwise `settings.contextWindow` / `autoCompactWindowCap` feed the
166
+ * CC math via getEffectiveContextWindowSize.
158
167
  */
159
168
  export function getAutoCompactThreshold(
160
169
  modelId: string,
161
170
  modelContextWindow?: number,
162
171
  modelMaxTokens?: number,
172
+ settings?: ModelConfigSettings,
163
173
  ): number {
164
- const effective = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens);
174
+ if (settings?.autoCompactThreshold !== undefined) return settings.autoCompactThreshold;
175
+ const effective = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens, settings);
165
176
  const autocompactThreshold = effective - AUTOCOMPACT_BUFFER_TOKENS;
166
177
 
167
- const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE;
168
- if (envPercent !== undefined) {
169
- const parsed = parseFloat(envPercent);
170
- if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 100) {
171
- const percentageThreshold = Math.floor(effective * (parsed / 100));
172
- return Math.min(percentageThreshold, autocompactThreshold);
173
- }
178
+ const percent = settings?.autoCompactPercent;
179
+ if (percent !== undefined && !Number.isNaN(percent) && percent > 0 && percent <= 100) {
180
+ const percentageThreshold = Math.floor(effective * (percent / 100));
181
+ return Math.min(percentageThreshold, autocompactThreshold);
174
182
  }
175
183
  return autocompactThreshold;
176
184
  }
@@ -4,11 +4,15 @@
4
4
  * Sources (claude-code repo):
5
5
  * - src/utils/context.ts — calculateContextPercentages
6
6
  * - src/services/compact/autoCompact.ts — calculateTokenWarningState, isAutoCompactEnabled
7
+ *
8
+ * No environment variables: the auto-compact on/off switch and blocking-limit
9
+ * override live in the `pi-plus` settings.json section (see settings.ts).
7
10
  */
8
11
 
9
12
  import {
10
13
  getAutoCompactThreshold,
11
14
  getEffectiveContextWindowSize,
15
+ type ModelConfigSettings,
12
16
  } from "./model-config.ts";
13
17
  import type { UsageSnapshot } from "./usage.ts";
14
18
 
@@ -16,17 +20,20 @@ export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000;
16
20
  export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000;
17
21
  export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000;
18
22
 
23
+ /** pi-plus settings keys that affect the warning state. */
24
+ export interface PercentagesSettings extends ModelConfigSettings {
25
+ /** Master switch for auto-compaction (default true). */
26
+ autoCompact?: boolean;
27
+ /** Override the blocking limit (absolute tokens). */
28
+ blockingLimit?: number;
29
+ }
30
+
19
31
  /**
20
- * CC: isAutoCompactEnabled — DISABLE_COMPACT / DISABLE_AUTO_COMPACT env vars.
21
- * (CC also consults a user-config flag; pi has no extension-readable
22
- * equivalent, so this port is env-only and defaults to enabled.)
32
+ * CC: isAutoCompactEnabled — CC consults env vars and a user-config flag;
33
+ * this port uses only the `autoCompact` settings.json flag (default true).
23
34
  */
24
- export function isAutoCompactEnabled(): boolean {
25
- const raw = (name: string) => process.env[name];
26
- const truthy = (v: string | undefined) => v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
27
- if (truthy(raw("DISABLE_COMPACT"))) return false;
28
- if (truthy(raw("DISABLE_AUTO_COMPACT"))) return false;
29
- return true;
35
+ export function isAutoCompactEnabled(settings?: Pick<PercentagesSettings, "autoCompact">): boolean {
36
+ return settings?.autoCompact !== false;
30
37
  }
31
38
 
32
39
  /**
@@ -58,17 +65,18 @@ export interface TokenWarningState {
58
65
  * CC: calculateTokenWarningState — percent left until the (auto-compact or
59
66
  * effective) threshold and the warning/error/blocking threshold crossings.
60
67
  * Warning and error buffers are both 20k in CC, so the two crossings are
61
- * identical unless env overrides differ.
68
+ * identical unless overrides differ.
62
69
  */
63
70
  export function calculateTokenWarningState(
64
71
  tokenUsage: number,
65
72
  modelId: string,
66
73
  modelContextWindow?: number,
67
74
  modelMaxTokens?: number,
75
+ settings?: PercentagesSettings,
68
76
  ): TokenWarningState {
69
- const autoCompact = isAutoCompactEnabled();
70
- const autoCompactThreshold = getAutoCompactThreshold(modelId, modelContextWindow, modelMaxTokens);
71
- const effectiveContextWindow = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens);
77
+ const autoCompact = isAutoCompactEnabled(settings);
78
+ const autoCompactThreshold = getAutoCompactThreshold(modelId, modelContextWindow, modelMaxTokens, settings);
79
+ const effectiveContextWindow = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens, settings);
72
80
  const threshold = autoCompact ? autoCompactThreshold : effectiveContextWindow;
73
81
 
74
82
  const percentLeft = Math.max(0, Math.round(((threshold - tokenUsage) / threshold) * 100));
@@ -77,10 +85,9 @@ export function calculateTokenWarningState(
77
85
  const errorThreshold = threshold - ERROR_THRESHOLD_BUFFER_TOKENS;
78
86
 
79
87
  const defaultBlockingLimit = effectiveContextWindow - MANUAL_COMPACT_BUFFER_TOKENS;
80
- const parsedOverride = process.env.CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE
81
- ? parseInt(process.env.CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE, 10)
82
- : NaN;
83
- const blockingLimit = !Number.isNaN(parsedOverride) && parsedOverride > 0 ? parsedOverride : defaultBlockingLimit;
88
+ const configuredBlocking = settings?.blockingLimit;
89
+ const blockingLimit =
90
+ configuredBlocking !== undefined && configuredBlocking > 0 ? configuredBlocking : defaultBlockingLimit;
84
91
 
85
92
  return {
86
93
  percentLeft,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * pi-plus settings, read through the pi ecosystem's standard settings
3
+ * functions (`getSetting` from @juanibiapina/pi-extension-settings) rather
4
+ * than by locating settings files ourselves.
5
+ *
6
+ * Storage: global values live in `~/.pi/agent/settings-extensions.json`,
7
+ * project values in `.pi/settings-extensions.json` (project wins) — both
8
+ * under a `"pi-plus"` extension section:
9
+ *
10
+ * ```json
11
+ * {
12
+ * "pi-plus": {
13
+ * "autoCompact": "true",
14
+ * "contextWindow": "256000",
15
+ * "autoCompactWindowCap": "200000",
16
+ * "maxOutputTokens": "8192",
17
+ * "disableOutputTokenCap": "false",
18
+ * "autoCompactThreshold": "235000",
19
+ * "autoCompactPercent": "50",
20
+ * "blockingLimit": "190000"
21
+ * }
22
+ * }
23
+ * ```
24
+ *
25
+ * Values are strings (the extension-settings convention); each accessor
26
+ * below parses and validates. Unset or invalid values fall back to the
27
+ * hardcoded CC-derived defaults in model-config.ts / percentages.ts.
28
+ *
29
+ * This plugin supports NO Claude Code environment variables — this settings
30
+ * system is the only configuration surface. (The plugin's own
31
+ * PI_PLUS_NO_AUTO_COMPACT and PI_PLUS_NO_LOG env switches live in index.ts /
32
+ * logger.ts.)
33
+ */
34
+
35
+ import { getSetting, type SettingStorageOptions } from "@juanibiapina/pi-extension-settings";
36
+ import type { ModelConfigSettings } from "./model-config.ts";
37
+
38
+ export interface PiPlusSettings extends ModelConfigSettings {
39
+ /** Master switch for auto-compaction (default true). */
40
+ autoCompact?: boolean;
41
+ /** Override the blocking limit (absolute tokens). */
42
+ blockingLimit?: number;
43
+ }
44
+
45
+ const EXTENSION_NAME = "pi-plus";
46
+
47
+ function raw(settingId: string, options?: SettingStorageOptions): string | undefined {
48
+ const value = getSetting(EXTENSION_NAME, settingId, undefined, options);
49
+ return value === undefined || value === "" ? undefined : value;
50
+ }
51
+
52
+ function positiveInt(settingId: string, options?: SettingStorageOptions): number | undefined {
53
+ const value = raw(settingId, options);
54
+ if (value === undefined) return undefined;
55
+ const parsed = Number(value);
56
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
57
+ return Math.floor(parsed);
58
+ }
59
+
60
+ function percentValue(settingId: string, options?: SettingStorageOptions): number | undefined {
61
+ const parsed = positiveInt(settingId, options);
62
+ if (parsed === undefined || parsed > 100) return undefined;
63
+ return parsed;
64
+ }
65
+
66
+ function booleanValue(settingId: string, options?: SettingStorageOptions): boolean | undefined {
67
+ const value = raw(settingId, options);
68
+ if (value === undefined) return undefined;
69
+ const normalized = value.trim().toLowerCase();
70
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
71
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
72
+ return undefined;
73
+ }
74
+
75
+ /** Load the merged pi-plus settings (local overrides global). */
76
+ export function loadPiPlusSettings(options?: SettingStorageOptions): PiPlusSettings {
77
+ const settings: PiPlusSettings = {};
78
+
79
+ const contextWindow = positiveInt("contextWindow", options);
80
+ const autoCompactWindowCap = positiveInt("autoCompactWindowCap", options);
81
+ const maxOutputTokens = positiveInt("maxOutputTokens", options);
82
+ const autoCompactThreshold = positiveInt("autoCompactThreshold", options);
83
+ const autoCompactPercent = percentValue("autoCompactPercent", options);
84
+ const blockingLimit = positiveInt("blockingLimit", options);
85
+ const autoCompact = booleanValue("autoCompact", options);
86
+ const disableOutputTokenCap = booleanValue("disableOutputTokenCap", options);
87
+
88
+ if (contextWindow !== undefined) settings.contextWindow = contextWindow;
89
+ if (autoCompactWindowCap !== undefined) settings.autoCompactWindowCap = autoCompactWindowCap;
90
+ if (maxOutputTokens !== undefined) settings.maxOutputTokens = maxOutputTokens;
91
+ if (autoCompactThreshold !== undefined) settings.autoCompactThreshold = autoCompactThreshold;
92
+ if (autoCompactPercent !== undefined) settings.autoCompactPercent = autoCompactPercent;
93
+ if (blockingLimit !== undefined) settings.blockingLimit = blockingLimit;
94
+ if (autoCompact !== undefined) settings.autoCompact = autoCompact;
95
+ if (disableOutputTokenCap !== undefined) settings.disableOutputTokenCap = disableOutputTokenCap;
96
+ return settings;
97
+ }