@diegopetrucci/pi-extensions 0.1.47 → 0.1.49

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
@@ -16,7 +16,7 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
16
16
  - [`inline-bash`](./extensions/inline-bash): Expands `!{command}` snippets in user prompts by running them through bash before the prompt reaches the agent.
17
17
  - [`illustrations-to-explain-things`](./extensions/illustrations-to-explain-things): Adds a skill for generating clean, absurd Xiaohei-style article illustrations, shot lists, image edits, and visual metaphors.
18
18
  - [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache disabled by default under the OS user cache directory, toggleable with `/librarian-cache`, configurable subagent model/thinking defaults via `/librarian-config`, and cached repos expiring after 7 days of non-use.
19
- - [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, plus OpenAI Codex 5-hour and 7-day usage when available.
19
+ - [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, optional `xp` marker, plus OpenAI Codex 5-hour and 7-day usage when available.
20
20
  - [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
21
21
  - [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
22
22
  - [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
@@ -4,7 +4,7 @@ A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cac
4
4
 
5
5
  When the `librarian` tool runs, it can cache/reuse repository checkouts locally. Use `/librarian-cache off` to force GitHub API/search and temporary fetched files only, or `/librarian-cache on` to re-enable cached local checkouts.
6
6
 
7
- The internal librarian subagent uses a lightweight auto-selected model by default and requests `medium` thinking. Use `/librarian-config` to set a persistent internal model or thinking-level preference.
7
+ The internal librarian subagent uses a fast auto-selected model by default, requests `low` thinking, and prompts its scout to batch independent GitHub probes in parallel. Use `/librarian-config` to set a persistent internal model or thinking-level preference.
8
8
 
9
9
  ## Install
10
10
 
@@ -39,7 +39,7 @@ Then reload pi:
39
39
  - Uses `gh` for GitHub search/API access
40
40
  - Uses cached local checkouts only when enabled
41
41
  - Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`
42
- - Configure internal subagent defaults with `/librarian-config status | model <provider/model|auto|current> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]`
42
+ - Configure internal subagent defaults with `/librarian-config status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]`
43
43
  - Cached repos are removed lazily after 7 days without use
44
44
 
45
45
  ## Commands
@@ -54,9 +54,8 @@ Then reload pi:
54
54
  ```text
55
55
  /librarian-config status
56
56
  /librarian-config model auto
57
- /librarian-config model current
58
57
  /librarian-config model anthropic/claude-haiku-4-5:medium
59
- /librarian-config thinking medium
58
+ /librarian-config thinking low
60
59
  /librarian-config clear model
61
60
  ```
62
61
 
@@ -33,9 +33,20 @@ type CacheMode = "disabled" | "enabled";
33
33
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
34
34
 
35
35
  const DEFAULT_CACHE_MODE: CacheMode = "disabled";
36
- const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium";
36
+ const DEFAULT_THINKING_LEVEL: ThinkingLevel = "low";
37
37
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
38
38
 
39
+ const PREFERRED_FAST_MODEL_PATTERNS = [
40
+ /\bgpt[-_. ]?5\.5(?:[-_. ].*)?\b(?:mini|nano|fast|lite)\b/,
41
+ /\bgpt[-_. ]?5(?:[-_. ].*)?\b(?:mini|nano|fast|lite)\b/,
42
+ /\bgpt[-_. ]?4(?:\.1|o)?(?:[-_. ].*)?\b(?:mini|nano)\b/,
43
+ /\bgemini\b.*\b(?:flash|flash-lite|lite)\b/,
44
+ /\bclaude\b.*\bhaiku\b/,
45
+ /\b(?:mistral|codestral)\b.*\b(?:small|mini|lite)\b/,
46
+ ];
47
+
48
+ const HEAVY_MODEL_PATTERN = /\b(?:opus|pro|ultra|max)\b/;
49
+
39
50
  type ToolCall = {
40
51
  id: string;
41
52
  name: string;
@@ -106,7 +117,7 @@ const LibrarianParams = Type.Object({
106
117
  ),
107
118
  model: Type.Optional(
108
119
  Type.String({
109
- description: "Optional model override for the internal librarian subagent. Use provider/model, auto, or current.",
120
+ description: "Optional model override for the internal librarian subagent. Use provider/model or auto.",
110
121
  }),
111
122
  ),
112
123
  thinkingLevel: Type.Optional(
@@ -308,7 +319,8 @@ function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
308
319
  function normalizeModelPreference(value: unknown): string | undefined {
309
320
  if (typeof value !== "string") return undefined;
310
321
  const trimmed = value.trim();
311
- if (!trimmed || trimmed.toLowerCase() === "auto") return undefined;
322
+ const normalized = trimmed.toLowerCase();
323
+ if (!trimmed || normalized === "auto" || normalized === "current") return undefined;
312
324
  return trimmed;
313
325
  }
314
326
 
@@ -317,7 +329,11 @@ function parseModelPreference(value: unknown): { model?: string; thinkingLevel?:
317
329
  if (!model) return {};
318
330
  const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
319
331
  if (!match?.[1]) return { model };
320
- return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
332
+ const baseModel = match[1].trim();
333
+ if (!baseModel || baseModel.toLowerCase() === "auto" || baseModel.toLowerCase() === "current") {
334
+ return { thinkingLevel: parseThinkingLevel(match[2]) };
335
+ }
336
+ return { model: baseModel, thinkingLevel: parseThinkingLevel(match[2]) };
321
337
  }
322
338
 
323
339
  async function readLibrarianPreferences(): Promise<LibrarianPreferences> {
@@ -415,11 +431,25 @@ function rankLibrarianModel(model: any): number {
415
431
  let score = modelCostScore(model) * 1_000_000;
416
432
  if (model.reasoning) score += 50;
417
433
  if (/\b(?:mini|nano|haiku|flash|lite|small|fast|instant)\b/.test(text)) score -= 10;
418
- if (/\b(?:opus|pro|ultra|max)\b/.test(text)) score += 1_000;
434
+ if (HEAVY_MODEL_PATTERN.test(text)) score += 1_000;
419
435
  if ((model.contextWindow ?? 0) < 32_000) score += 100;
420
436
  return score;
421
437
  }
422
438
 
439
+ function isPreferredFastLibrarianModel(model: any): boolean {
440
+ const text = `${model.provider ?? ""} ${model.id ?? ""} ${model.name ?? ""}`.toLowerCase();
441
+ if (HEAVY_MODEL_PATTERN.test(text)) return false;
442
+ return PREFERRED_FAST_MODEL_PATTERNS.some((pattern) => pattern.test(text));
443
+ }
444
+
445
+ function rankPreferredFastLibrarianModel(model: any): number {
446
+ const text = `${model.provider ?? ""} ${model.id ?? ""} ${model.name ?? ""}`.toLowerCase();
447
+ let score = rankLibrarianModel(model);
448
+ const patternIndex = PREFERRED_FAST_MODEL_PATTERNS.findIndex((pattern) => pattern.test(text));
449
+ if (patternIndex >= 0) score += patternIndex * 100;
450
+ return score;
451
+ }
452
+
423
453
  async function findAvailableModel(
424
454
  ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
425
455
  modelPreference: string,
@@ -453,21 +483,6 @@ async function selectLibrarianModel(
453
483
  thinkingLevel: ThinkingLevel,
454
484
  ): Promise<{ model: any; details: LibrarianModelDetails }> {
455
485
  const normalized = modelPreference?.trim();
456
- if (normalized?.toLowerCase() === "current") {
457
- if (!ctx.model) throw new Error("Librarian model=current needs an active pi model, but ctx.model is unavailable.");
458
- return {
459
- model: ctx.model,
460
- details: {
461
- modelRef: modelRef(ctx.model),
462
- provider: ctx.model.provider,
463
- modelId: ctx.model.id,
464
- thinkingLevel,
465
- autoSelected: false,
466
- selectionReason: "Using the caller's current model because librarian model=current is configured.",
467
- },
468
- };
469
- }
470
-
471
486
  if (normalized) {
472
487
  const matched = await findAvailableModel(ctx, normalized);
473
488
  if (matched) {
@@ -486,15 +501,18 @@ async function selectLibrarianModel(
486
501
  }
487
502
 
488
503
  const available = await ctx.modelRegistry.getAvailable();
489
- const currentProvider = ctx.model?.provider;
490
- const sameProvider = currentProvider ? available.filter((model) => model.provider === currentProvider) : [];
491
- const candidates = sameProvider.length > 0 ? sameProvider : available;
492
- const winner = [...candidates].sort((a, b) => rankLibrarianModel(a) - rankLibrarianModel(b))[0] ?? ctx.model;
504
+ const preferred = available.filter(isPreferredFastLibrarianModel);
505
+ const winner = preferred.length > 0
506
+ ? [...preferred].sort((a, b) => rankPreferredFastLibrarianModel(a) - rankPreferredFastLibrarianModel(b))[0]
507
+ : [...available].sort((a, b) => rankLibrarianModel(a) - rankLibrarianModel(b))[0];
493
508
  if (!winner) {
494
509
  throw new Error("No authenticated models are available for Librarian. Log in or configure an API key first.");
495
510
  }
496
511
 
497
512
  const fallbackText = normalized ? ` Configured model ${normalized} was unavailable, so Librarian fell back to auto-selection.` : "";
513
+ const selectionReason = preferred.length > 0
514
+ ? `Selected a preferred fast Librarian model.${fallbackText}`
515
+ : `Selected the cheapest available model because no preferred fast Librarian model was available.${fallbackText}`;
498
516
  return {
499
517
  model: winner,
500
518
  details: {
@@ -503,7 +521,7 @@ async function selectLibrarianModel(
503
521
  modelId: winner.id,
504
522
  thinkingLevel,
505
523
  autoSelected: true,
506
- selectionReason: `Selected the cheapest available model${sameProvider.length > 0 ? " on the current provider" : ""}.${fallbackText}`,
524
+ selectionReason,
507
525
  },
508
526
  };
509
527
  }
@@ -621,7 +639,7 @@ function buildSystemPrompt(options: {
621
639
  ? `\nLocal checkout cache is ENABLED for this call.\n- Cache root: ${options.cacheRoot}\n- Use checkout path pattern: ${options.cacheRoot}/github.com/<owner>/<repo>\n- Reuse an existing checkout when it has a .git directory. Fetch/prune before relying on it: git -C "$DIR" fetch --all --prune --tags --quiet\n- If missing, clone with gh repo clone "$REPO" "$DIR" (or git clone https://github.com/$REPO.git "$DIR").\n- If a ref/branch/SHA is requested, fetch it and check it out locally before citing files from that ref.\n- After using a checkout, update its cache marker: touch "$DIR/${CACHE_MARKER_FILE}"\n- Prefer local rg/read inside cached checkouts once a repo is cloned, and cite absolute cached paths with line ranges.\n- Clone only repositories that are relevant to the query; do not bulk-clone broad owner/org scopes unless necessary.`
622
640
  : `\nLocal checkout cache is DISABLED for this call.\n- Do not clone repositories.\n- Use gh search/API/tree/contents calls and cache only necessary proof files under ${options.workspace}/repos/<owner>/<repo>/<path>.`;
623
641
 
624
- return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse only the available bash/read tools. Use gh, jq, rg, find/fd, ls, stat, mkdir, base64, and nl -ba for GitHub reconnaissance and numbered evidence. Use read for focused local file inspection.\n\nWorkspace: ${options.workspace}\nDefault gh search limit: ${options.maxSearchResults}\nTurn budget: ${MAX_TURNS} turns total, including your final answer. Stop searching once you have enough evidence.\n${cacheSection}\n\nNon-negotiable constraints:\n- Never treat gh search snippets as proof by themselves. Use fetched files or local checkouts for code-content claims.\n- Keep temporary workspace writes under ${options.workspace}/repos unless local checkout cache is enabled, in which case writes under the cache root are also allowed.
642
+ return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse only the available bash/read tools. Use gh, jq, rg, find/fd, ls, stat, mkdir, base64, and nl -ba for GitHub reconnaissance and numbered evidence. Use read for focused local file inspection.\n\nWorkspace: ${options.workspace}\nDefault gh search limit: ${options.maxSearchResults}\nTurn budget: ${MAX_TURNS} turns total, including your final answer. Stop searching once you have enough evidence.\n${cacheSection}\n\nPerformance guidance:\n- Prefer fast, parallel exploration. When probes are independent, issue multiple separate tool calls in the same assistant turn instead of waiting for each result one-by-one.\n- Aim for 4-8 independent bash/read calls in an exploration turn when useful: parallel gh searches, tree probes, contents fetches, and targeted file reads.\n- Keep dependent work sequential, and avoid concurrent writes to the same file, checkout, or cache directory.\n\nNon-negotiable constraints:\n- Never treat gh search snippets as proof by themselves. Use fetched files or local checkouts for code-content claims.\n- Keep temporary workspace writes under ${options.workspace}/repos unless local checkout cache is enabled, in which case writes under the cache root are also allowed.
625
643
  - A runtime guard blocks destructive shell commands, credential/environment inspection, and reads outside the workspace/cache.\n- Never paste whole files. Use short snippets only when they clarify the evidence.\n- If evidence is partial or access fails (404/403), state the limitation clearly.\n- Do not present anything as fact unless it appeared in tool output or in a file you read.\n\nRecommended search flow:\n1. If symbols/text are known, start with gh search code and the provided repo/owner filters.\n2. If a repo is known but paths are unclear, resolve the default branch and inspect the git tree or contents API.\n3. Fetch or clone only the files/repos required to prove the answer.\n4. Use rg/read/nl -ba locally to produce stable path and line evidence.\n\nUseful gh patterns:\n- gh repo view "$REPO" --json defaultBranchRef --jq '.defaultBranchRef.name'\n- gh search code '<terms>' --json path,repository,sha,url,textMatches --limit ${options.maxSearchResults}\n- gh api "repos/$REPO/git/trees/$REF?recursive=1" > tree.json\n- gh api "repos/$REPO/contents/$FILE?ref=$REF" --jq .content | tr -d '\\n' | base64 --decode > "repos/$REPO/$FILE"\n- rg -n '<pattern>' '<local path>'\n- nl -ba '<local file>' | sed -n '10,30p'\n\nOutput format, exact order:\n## Summary\n1-3 concise sentences.\n## Locations\n- \`path\` or \`path:lineStart-lineEnd\` — what is here and why it matters; include GitHub URL when useful. If nothing relevant is found, write \`- (none)\`.\n## Evidence\n- \`path\` or \`path:lineStart-lineEnd\` — what this proves. Include concise snippets only if useful.\n## Searched\nOnly include when incomplete/not found or when the search path matters. List queries, filters, and probes used.\n## Next steps\nOptional: 1-3 narrow follow-up checks for remaining ambiguity.`;
626
644
  }
627
645
 
@@ -767,16 +785,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
767
785
  if (action === "model") {
768
786
  const value = rest.join(" ").trim();
769
787
  if (!value) {
770
- notifyCommand(ctx, "Usage: /librarian-config model <provider/model|auto|current>", "warning");
788
+ notifyCommand(ctx, "Usage: /librarian-config model <provider/model|auto>", "warning");
771
789
  return;
772
790
  }
773
791
  const normalized = value.toLowerCase();
792
+ if (normalized === "current" || normalized.startsWith("current:")) {
793
+ notifyCommand(ctx, "Librarian model=current is no longer supported. Use /librarian-config model auto or choose an explicit provider/model.", "warning");
794
+ return;
795
+ }
774
796
  const parsedModel = parseModelPreference(value);
775
797
  const next = normalized === "auto" || normalized === "clear" || normalized === "default"
776
798
  ? { ...currentPreferences(), model: undefined }
777
799
  : {
778
800
  ...currentPreferences(),
779
- model: normalized === "current" ? "current" : parsedModel.model,
801
+ model: parsedModel.model,
780
802
  thinkingLevel: parsedModel.thinkingLevel ?? thinkingPreference,
781
803
  };
782
804
  const warning = await savePreferences(next);
@@ -817,7 +839,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
817
839
  return;
818
840
  }
819
841
 
820
- notifyCommand(ctx, "Usage: /librarian-config status | model <provider/model|auto|current> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
842
+ notifyCommand(ctx, "Usage: /librarian-config status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
821
843
  },
822
844
  });
823
845
 
@@ -896,7 +918,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
896
918
  description:
897
919
  "GitHub research scout for coding and personal-assistant tasks. Use when the answer likely lives in GitHub repos, exact repo/path locations are unknown, or you'd otherwise do exploratory gh search/tree probes plus local rg/read inspection. Librarian uses an optional 7-day local checkout cache that is disabled by default; toggle it with /librarian-cache. Configure its internal subagent defaults with /librarian-config.",
898
920
  promptSnippet:
899
- "Research GitHub repositories with evidence-first path and line citations; local checkout cache is disabled by default and user-toggleable with /librarian-cache. Internal subagent defaults are user-configurable with /librarian-config and default to medium thinking.",
921
+ "Research GitHub repositories with evidence-first path and line citations; local checkout cache is disabled by default and user-toggleable with /librarian-cache. Internal subagent defaults are user-configurable with /librarian-config and default to low thinking.",
900
922
  promptGuidelines: [
901
923
  "Use librarian when the answer likely requires exploratory GitHub repository search or line-cited evidence from external repos.",
902
924
  "Do not use librarian for files already present in the current workspace unless the user asks for external GitHub research.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-librarian",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -12,6 +12,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
12
12
  - red `DUMB ZONE` indicator when context usage is above 200k tokens
13
13
  - current model and thinking level
14
14
  - OpenAI Codex 5-hour and 7-day usage when available
15
+ - `xp` marker when Pi experimental features are enabled
15
16
 
16
17
  ## Layout
17
18
 
@@ -41,6 +42,12 @@ When using `openai-codex`, the bottom-left line also includes subscription usage
41
42
  44.1% · 5h 12% · 7d 38%
42
43
  ```
43
44
 
45
+ When `PI_EXPERIMENTAL=1`, the bottom-left line also includes an experimental marker:
46
+
47
+ ```text
48
+ 44.1% · xp
49
+ ```
50
+
44
51
  On narrow terminals it falls back to one item per line.
45
52
 
46
53
  ## Install
@@ -107,6 +114,11 @@ Example:
107
114
  "label": "7d"
108
115
  }
109
116
  }
117
+ },
118
+ "experimentalMarker": {
119
+ "enabled": true,
120
+ "label": "xp",
121
+ "color": "warning"
110
122
  }
111
123
  }
112
124
  ```
@@ -147,6 +159,16 @@ Disable one session-limit window:
147
159
  }
148
160
  ```
149
161
 
162
+ Disable the experimental-features marker:
163
+
164
+ ```json
165
+ {
166
+ "experimentalMarker": {
167
+ "enabled": false
168
+ }
169
+ }
170
+ ```
171
+
150
172
  ### Config fields
151
173
 
152
174
  - `context.showPercent`: show the context percentage
@@ -161,6 +183,9 @@ Disable one session-limit window:
161
183
  - `codexUsage.windows.primary.label`: label for the primary usage window
162
184
  - `codexUsage.windows.secondary.enabled`: show the secondary usage window
163
185
  - `codexUsage.windows.secondary.label`: label for the secondary usage window
186
+ - `experimentalMarker.enabled`: show the marker when `PI_EXPERIMENTAL=1`
187
+ - `experimentalMarker.label`: marker text
188
+ - `experimentalMarker.color`: theme color for the marker (`error`, `warning`, `accent`, `text`, or `dim`)
164
189
 
165
190
  ## What it shows
166
191
 
@@ -168,6 +193,7 @@ Disable one session-limit window:
168
193
  - **Top right:** current repo directory name
169
194
  - **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 200k context tokens
170
195
  - **Bottom left on `openai-codex`:** current context usage percentage plus 5-hour and 7-day Codex usage
196
+ - **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
171
197
  - **Bottom right:** model id and thinking level
172
198
 
173
199
  ## Publishing notes
@@ -181,5 +207,6 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
181
207
  - Shows only context percentage, not context window size.
182
208
  - Shows `DUMB ZONE` only while context usage is above 200k tokens.
183
209
  - Shows the model id rather than a provider-specific display label.
210
+ - Shows `xp` when `PI_EXPERIMENTAL=1`.
184
211
  - For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint.
185
212
  - Usage is cached briefly in memory and refreshed after turns.
@@ -41,6 +41,11 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
41
41
  },
42
42
  },
43
43
  },
44
+ experimentalMarker: {
45
+ enabled: true,
46
+ label: "xp",
47
+ color: "warning",
48
+ },
44
49
  };
45
50
 
46
51
  const DUMB_ZONE_COLORS = new Set<DumbZoneColor>([
@@ -82,6 +87,11 @@ interface MinimalFooterConfig {
82
87
  };
83
88
  };
84
89
  };
90
+ experimentalMarker: {
91
+ enabled: boolean;
92
+ label: string;
93
+ color: DumbZoneColor;
94
+ };
85
95
  }
86
96
 
87
97
  type UsageSessionState = {
@@ -120,6 +130,7 @@ function mergeConfig(
120
130
  const codexUsage = overrides.codexUsage;
121
131
  const primaryWindow = codexUsage?.windows?.primary;
122
132
  const secondaryWindow = codexUsage?.windows?.secondary;
133
+ const experimentalMarker = overrides.experimentalMarker;
123
134
 
124
135
  return {
125
136
  context: {
@@ -161,6 +172,17 @@ function mergeConfig(
161
172
  },
162
173
  },
163
174
  },
175
+ experimentalMarker: {
176
+ enabled: normalizeBoolean(
177
+ experimentalMarker?.enabled,
178
+ base.experimentalMarker.enabled,
179
+ ),
180
+ label: normalizeLabel(experimentalMarker?.label, base.experimentalMarker.label),
181
+ color: normalizeDumbZoneColor(
182
+ experimentalMarker?.color,
183
+ base.experimentalMarker.color,
184
+ ),
185
+ },
164
186
  };
165
187
  }
166
188
 
@@ -213,6 +235,10 @@ function shouldShowCodexUsage(config: MinimalFooterConfig): boolean {
213
235
  );
214
236
  }
215
237
 
238
+ function shouldShowExperimentalMarker(config: MinimalFooterConfig): boolean {
239
+ return config.experimentalMarker.enabled && process.env.PI_EXPERIMENTAL === "1";
240
+ }
241
+
216
242
  function clearUsageState(state: UsageSessionState): void {
217
243
  state.snapshot = undefined;
218
244
  state.lastFetchedAt = undefined;
@@ -317,6 +343,10 @@ export default function (pi: ExtensionAPI) {
317
343
  if (state.config.context.showPercent) contextParts.push(theme.fg("dim", context));
318
344
  if (inDumbZone) contextParts.push(theme.fg(dumbZone.color, dumbZone.label));
319
345
  if (usageSummary) contextParts.push(theme.fg("dim", usageSummary));
346
+ if (shouldShowExperimentalMarker(state.config)) {
347
+ const marker = state.config.experimentalMarker;
348
+ contextParts.push(theme.fg(marker.color, marker.label));
349
+ }
320
350
  const contextStyled = contextParts.join(theme.fg("dim", " · "));
321
351
  const modelStyled = theme.fg("dim", modelText);
322
352
 
@@ -22,5 +22,10 @@
22
22
  "label": "7d"
23
23
  }
24
24
  }
25
+ },
26
+ "experimentalMarker": {
27
+ "enabled": true,
28
+ "label": "xp",
29
+ "color": "warning"
25
30
  }
26
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": [
6
6
  "pi-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
5
5
  "keywords": [
6
6
  "pi-package",