pi-skillful 0.3.14 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,20 +1,43 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.14 - 2026-07-17
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- ## 0.3.13 - 2026-07-17
5
+ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
6
6
 
7
- ## 0.3.12 - 2026-07-12
7
+ ## [Unreleased]
8
8
 
9
- ## 0.3.11 - 2026-07-01
9
+ ## [0.5.0] - 2026-10-09
10
10
 
11
- ## 0.3.9 - 2026-06-14
11
+ ### Fixed
12
12
 
13
- All notable changes to this project will be documented in this file.
13
+ - Read the current theme when rendering session-toggle borders and fit the complete border at very narrow widths. Extend editor composition coverage for paste, padding, indicators, and disposal.
14
14
 
15
- This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
15
+ ### Changed
16
16
 
17
- ## [Unreleased]
17
+ - Update skill visibility and session toggles through Pi's structured prompt API instead of replacing the rendered system prompt. Preserve unrelated sections and deliberate full-prompt overrides.
18
+ - Update the shared Pi development and contract-test baseline to 1.1.0; require Node.js >=22.19.0 to match the host runtime. Pi remains a host-supplied peer dependency.
19
+ - Share install telemetry mechanics through `@mocito/install-telemetry` while preserving Pi-specific settings and state paths.
20
+
21
+ ### Fixed
22
+
23
+ - Apply visibility settings to Pi 1.1.0's wrapped skills section, including when file readers are hidden behind codemode.
24
+ - Keep frontmatter and earlier extensions' visibility restrictions when session toggle slots are active.
25
+ - Restore hidden-skill startup colors on Pi 1.x while preserving theme changes, expanded resource paths, and reloads.
26
+ - Let `enableInstallTelemetry: false` override an enabled `PI_TELEMETRY` environment flag.
27
+
28
+ ## [0.4.0] - 2026-07-28
29
+
30
+ ### Added
31
+
32
+ - Progressive skill loading: discover `.agents/skills/` directories from all ancestor directories (like `AGENTS.md`), not just within the git repo boundary. Uses the `resources_discover` event to contribute missed paths.
33
+
34
+ ## [0.3.14] - 2026-07-17
35
+
36
+ ### Fixed
37
+
38
+ - Restore red/dim coloring for hidden and visible skills in Pi's startup `[Skills]` list while preserving project-trust-aware settings.
39
+
40
+ ## [0.3.13] - 2026-07-17
18
41
 
19
42
  ### Fixed
20
43
 
@@ -22,16 +45,23 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
22
45
  - Let an explicit project `toggleModifier: "alt"` override a non-default global modifier; unsupported explicit values now follow the documented `"alt"` fallback.
23
46
  - Consume only configured modifier-slot combinations in the prompt editor instead of registering all 54 possible shortcuts.
24
47
  - Propagate focus and custom-editor hooks through the session-toggle editor wrapper for IME and extension compatibility.
25
- - Restore red/dim coloring for hidden and visible skills in Pi's startup `[Skills]` list while preserving project-trust-aware settings.
26
48
  - Remove the stale package-local `package-lock.json` in favor of the monorepo root lockfile.
27
- - Preserve configured hidden skills at session start. Skillful no longer treats a temporarily incomplete loaded-skill list as authoritative and removes global or project visibility settings.
28
49
 
29
- ## [0.3.10] - 2026-07-01
50
+ ## [0.3.12] - 2026-07-13
30
51
 
31
52
  ### Fixed
32
53
 
33
- - Color hidden skills in the startup `[Skills]` list on `@earendil-works/pi-coding-agent` 0.80+. The 0.80 release moved the loaded-resources container out of `chatContainer`, so the existing prototype patch walked the wrong container and the red/dim colorization no longer applied. The patch now also walks `loadedResourcesContainer`, where 0.80 actually renders the `[Skills]` section.
34
- - Bumped dev dependency range to `^0.80.0` to match the new floor.
54
+ - Preserve configured hidden skills at session start instead of pruning settings from a temporarily incomplete loaded-skill list.
55
+
56
+ ## [0.3.11] - 2026-07-01
57
+
58
+ ### Fixed
59
+
60
+ - Color hidden skills in the startup `[Skills]` list on Pi 0.80+, which moved the rendered resources into `loadedResourcesContainer`.
61
+
62
+ ### Changed
63
+
64
+ - Raise the Pi development dependency floor to `^0.80.0`.
35
65
 
36
66
  ## [0.3.9] - 2026-06-14
37
67
 
package/CONTRIBUTING.md CHANGED
@@ -33,6 +33,14 @@ pi -e /path/to/pi-mono/packages/pi-skillful
33
33
 
34
34
  ## Pull request checklist
35
35
 
36
+ For prompt changes, also run the shared real-session contracts from the monorepo root:
37
+
38
+ ```bash
39
+ node --import tsx --test tests/structured-prompts.test.mjs
40
+ ```
41
+
42
+ These load both Skillful and Scout through Pi's SDK, inspect serialized provider requests and session history, and exercise the toggle editor wrapper. They use disposable profiles, synthetic credentials, temporary reference directories, and mocked provider responses; they do not use a live model or existing user settings. Coverage includes extension load order, explicit prompt overrides, hidden skill invocation, codemode, reload, resume, fork/tree navigation, and providers that collapse system updates.
43
+
36
44
  Before opening a pull request:
37
45
 
38
46
  - Run `npm run check`.
@@ -52,3 +60,9 @@ Before opening a pull request:
52
60
  ## Code of conduct
53
61
 
54
62
  This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
63
+ ## Pi 1.1.0 terminal audit
64
+
65
+ From the monorepo root, run `node --import tsx --test tests/tui-contracts.test.mjs`
66
+ alongside the package tests. `tests/TUI_AUDIT.md` records coverage, known host
67
+ limitations, and the disposable-profile physical-terminal acceptance procedure.
68
+ The startup patch remains isolated; no unreleased cursor API is required.
package/README.md CHANGED
@@ -6,8 +6,9 @@
6
6
 
7
7
  Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without editing their source files.
8
8
 
9
- `pi-skillful` provides three focused upgrades:
9
+ `pi-skillful` provides four focused upgrades:
10
10
 
11
+ - **Progressive skill loading**: discover `.agents/skills/` from every ancestor directory (like `AGENTS.md`), not just within the git repo boundary.
11
12
  - **Inline skill invocation**: invoke one or more skills anywhere in a prompt with `/skill:name`.
12
13
  - **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt while keeping them explicitly invokable.
13
14
  - **Session skill toggles**: assign up to nine skills to number slots and toggle model visibility while writing a prompt.
@@ -17,6 +18,14 @@ Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without e
17
18
 
18
19
  ## Features
19
20
 
21
+ ### Progressive skill loading
22
+
23
+ Pi discovers `.agents/skills/` directories in ancestor folders, but stops at the git repo root — skills above the repo boundary are invisible. `pi-skillful` removes that boundary by walking the full ancestor chain to the filesystem root, mirroring how `AGENTS.md` context files work.
24
+
25
+ No configuration needed. When a git repo is detected, `pi-skillful` automatically contributes `.agents/skills/` directories from parent directories above the repo root (excluding `~/.agents/skills/`, which Pi already loads globally). In projects without a git repo, Pi already walks to the filesystem root and this feature has no effect.
26
+
27
+ Skill name collisions follow Pi's first-wins rule: skills discovered closer to the working directory take precedence over skills with the same name found higher up.
28
+
20
29
  ### Inline skill invocation
21
30
 
22
31
  Vanilla Pi expands `/skill:name` only when it appears at the beginning of the prompt. `pi-skillful` expands known skill markers anywhere in the prompt, including multiple skills:
@@ -93,6 +102,19 @@ Configured slots appear on the prompt editor's top border as `N skill-name`. Pro
93
102
 
94
103
  On app startup, non-hidden skills are active and hidden skills are inactive. Within a running Pi process, `/new` preserves the current toggle state for the new session. Resuming, forking, cloning, reloading, or restarting Pi resets toggle state from settings. Inline `/skill:name` invocation remains explicit and works even when that skill is inactive. Skills bundled in Pi packages are never modified by these toggles.
95
104
 
105
+ ### Prompt updates and extension composition
106
+
107
+ Toggle-border colors follow the current theme without another toggle or reload.
108
+ At very narrow widths, the complete border is clipped to the available columns.
109
+
110
+ Visibility and toggles use Pi's structured `systemPromptOptions.skills` API (Pi 1.1.0 or newer), not text replacement of the full system prompt. Pi records skill-list changes in the session transcript, alongside independent updates from other extensions such as Scout.
111
+
112
+ An active slot can lift a restriction from `skillful.hiddenSkills`, but does not clear `disable-model-invocation` frontmatter or a restriction applied by an earlier prompt handler. Later handlers see the updated list and can apply their own policy. Package-bundled skills remain outside Skillful's visibility controls and toggle slots.
113
+
114
+ A deliberate full-prompt override from another extension (`systemPrompt` or `forceSystemPrompt`) takes precedence. Skillful updates the structured skill list but does not rewrite that override; its author controls any skill descriptions embedded in the replacement text. Explicit skill invocation remains available.
115
+
116
+ Structured changes let Pi update individual prompt sections. Providers that cannot accept mid-conversation system changes may still need a full prompt checkpoint; cache savings are not guaranteed.
117
+
96
118
  ## Installation
97
119
 
98
120
  Install from npm:
@@ -139,7 +161,7 @@ This package is source-distributed. Pi loads the TypeScript extensions directly
139
161
 
140
162
  Requirements:
141
163
 
142
- - Node.js >= 20.6.0
164
+ - Node.js >= 22.19.0
143
165
  - npm for local development commands
144
166
 
145
167
  Common commands:
package/SECURITY.md CHANGED
@@ -20,3 +20,13 @@ The maintainer will acknowledge reports as soon as practical and coordinate disc
20
20
  ## Security model
21
21
 
22
22
  `pi-skillful` is a Pi package. Pi extensions execute with the same permissions as the local user running Pi. Users should review installed Pi packages and only install packages from sources they trust.
23
+
24
+ At startup, `@mocito/install-telemetry` sends a best-effort install/update ping to the configured telemetry endpoint once per package version unless CI, Pi offline/telemetry settings, or `enableInstallTelemetry: false` disables it. It contains only the package name/version and parsed platform/runtime/architecture; it does not include prompts, paths, configuration values, credentials, or provider responses.
25
+
26
+ ### Progressive skill loading and trust boundary
27
+
28
+ `pi-skillful` extends Pi's skill discovery to ancestor directories above the git repository root, loading `.agents/skills/` from every parent directory up to the filesystem root (excluding `~/.agents/skills/`, which Pi already loads globally).
29
+
30
+ This means skills placed in a shared or writable ancestor directory — for example, `/home/shared/.agents/skills/` or `/tmp/.agents/skills/` — are automatically available as skill instructions when Pi runs in any nested repository. Review the contents and ownership of `.agents/skills/` directories above your repositories, as they are now part of the effective skill set.
31
+
32
+ To disable progressive loading without removing the package, uninstall `pi-skillful` or use Pi's built-in `--no-skills` flag to suppress all skill loading for a session.
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import inlineSkillInvocation from "../src/extensions/inline-skill-invocation.js";
3
+ import progressiveSkills from "../src/extensions/progressive-skills.js";
3
4
  import { reportInstallTelemetry } from "../src/install-telemetry.js";
4
5
  import skillVisibility from "../src/extensions/skill-visibility.js";
5
6
  import sessionSkillToggles from "../src/extensions/session-skill-toggles.js";
@@ -7,6 +8,7 @@ import sessionSkillToggles from "../src/extensions/session-skill-toggles.js";
7
8
  export default function piSkillful(pi: ExtensionAPI) {
8
9
  reportInstallTelemetry();
9
10
 
11
+ progressiveSkills(pi);
10
12
  inlineSkillInvocation(pi);
11
13
  skillVisibility(pi);
12
14
  sessionSkillToggles(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skillful",
3
- "version": "0.3.14",
3
+ "version": "0.5.0",
4
4
  "description": "Pi package with skill invocation and visibility improvements.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,10 +45,10 @@
45
45
  "@earendil-works/pi-tui": "*"
46
46
  },
47
47
  "devDependencies": {
48
- "@earendil-works/pi-coding-agent": "^0.80.0",
49
- "@earendil-works/pi-tui": "^0.80.0",
50
- "@types/node": "^26.1.0",
51
- "typescript": "^6.0.3"
48
+ "@earendil-works/pi-coding-agent": "1.1.0",
49
+ "@earendil-works/pi-tui": "1.1.0",
50
+ "@types/node": "^26.6.3",
51
+ "typescript": "^7.0.2"
52
52
  },
53
53
  "scripts": {
54
54
  "check": "tsc --noEmit",
@@ -60,6 +60,9 @@
60
60
  "access": "public"
61
61
  },
62
62
  "engines": {
63
- "node": ">=20.6.0"
63
+ "node": ">=22.19.0"
64
+ },
65
+ "dependencies": {
66
+ "@mocito/install-telemetry": "0.1.1"
64
67
  }
65
68
  }
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+
6
+ /**
7
+ * Walks from cwd up through all ancestor directories (no git boundary)
8
+ * and discovers `.agents/skills/` directories that Pi's built-in discovery
9
+ * misses because it stops at the git repo root.
10
+ *
11
+ * This mirrors how AGENTS.md context files are loaded from every parent
12
+ * directory, not just within the repo boundary.
13
+ */
14
+ export default function progressiveSkills(pi: ExtensionAPI) {
15
+ pi.on("resources_discover", async (event) => {
16
+ const gitRoot = findGitRoot(event.cwd);
17
+
18
+ // No git repo — Pi already walks to filesystem root, nothing to add.
19
+ if (!gitRoot) return;
20
+
21
+ const resolvedGitRoot = resolve(gitRoot);
22
+ const homeAgentsSkills = resolve(homedir(), ".agents", "skills");
23
+ const skillPaths: string[] = [];
24
+
25
+ // Walk from git root's parent up to filesystem root.
26
+ // Order: git-parent first (closer to cwd = higher priority via first-wins).
27
+ let dir = dirname(resolvedGitRoot);
28
+ while (true) {
29
+ const agentsSkills = join(dir, ".agents", "skills");
30
+ const resolved = resolve(agentsSkills);
31
+
32
+ // Skip ~/.agents/skills (handled globally by Pi) and non-existent dirs.
33
+ if (resolved !== homeAgentsSkills && existsSync(resolved)) {
34
+ skillPaths.push(resolved);
35
+ }
36
+
37
+ const parent = dirname(dir);
38
+ if (parent === dir) break;
39
+ dir = parent;
40
+ }
41
+
42
+ if (skillPaths.length === 0) return;
43
+ return { skillPaths };
44
+ });
45
+ }
46
+
47
+ function findGitRoot(startDir: string): string | undefined {
48
+ let dir = resolve(startDir);
49
+ while (true) {
50
+ if (existsSync(join(dir, ".git"))) return dir;
51
+ const parent = dirname(dir);
52
+ if (parent === dir) return undefined;
53
+ dir = parent;
54
+ }
55
+ }
@@ -3,7 +3,6 @@ import {
3
3
  type AppKeybinding,
4
4
  type ExtensionAPI,
5
5
  type KeybindingsManager,
6
- type Skill,
7
6
  type Theme,
8
7
  } from "@earendil-works/pi-coding-agent";
9
8
  import type { AutocompleteProvider, EditorComponent, EditorTheme, Focusable, KeyId, TUI } from "@earendil-works/pi-tui";
@@ -16,7 +15,6 @@ import {
16
15
  type SkillToggleModifier,
17
16
  type SkillToggleSlot,
18
17
  } from "../config.js";
19
- import { replaceSkillsSection } from "../skill-prompt.js";
20
18
  import { isTopLevelSkill, listLoadedSkills } from "../skills.js";
21
19
 
22
20
  const STORE_KEY = Symbol.for("pi-skillful.sessionSkillTogglesStore");
@@ -40,7 +38,7 @@ interface SessionToggleState {
40
38
  installedEditor: boolean;
41
39
  previousEditorFactory: SkillfulEditorFactory | undefined;
42
40
  activeTui: TUI | undefined;
43
- theme: Theme | undefined;
41
+ theme: (() => Theme) | undefined;
44
42
  notify: (message: string, type?: "info" | "warning" | "error") => void;
45
43
  }
46
44
 
@@ -79,7 +77,7 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
79
77
  installedEditor: false,
80
78
  previousEditorFactory: undefined,
81
79
  activeTui: undefined,
82
- theme: ctx.ui.theme,
80
+ theme: () => ctx.ui.theme,
83
81
  notify: ctx.ui.notify.bind(ctx.ui),
84
82
  };
85
83
 
@@ -90,13 +88,13 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
90
88
  pi.on("before_agent_start", (event) => {
91
89
  if (state.slots.length === 0 || !event.systemPromptOptions.skills?.length) return;
92
90
 
93
- const updatedSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
94
- isTopLevelSkill(skill) ? { ...skill, disableModelInvocation: !isSkillActive(normalizeSkillName(skill.name)) } : skill,
91
+ // Active slots lift only Skillful's own restriction, never frontmatter or
92
+ // restrictions already applied by another prompt handler.
93
+ event.systemPromptOptions.skills = event.systemPromptOptions.skills.map((skill) =>
94
+ isTopLevelSkill(skill) && !isSkillActive(normalizeSkillName(skill.name))
95
+ ? { ...skill, disableModelInvocation: true }
96
+ : skill,
95
97
  );
96
-
97
- const systemPrompt = replaceSkillsSection(event.systemPrompt, updatedSkills);
98
- if (!systemPrompt) return;
99
- return { systemPrompt };
100
98
  });
101
99
 
102
100
  pi.on("session_shutdown", (event, ctx) => {
@@ -373,7 +371,7 @@ function renderToggleBorder(width: number, borderColor: (text: string) => string
373
371
  const fittedContent = truncateToWidth(renderToggleSegments(available), available, "");
374
372
  const used = BORDER_PREFIX_WIDTH + visibleWidth(fittedContent) + BORDER_SUFFIX_WIDTH;
375
373
  const fill = borderColor("─".repeat(Math.max(0, width - used)));
376
- return `${borderColor(BORDER_PREFIX)}${fittedContent}${BORDER_SUFFIX}${fill}`;
374
+ return truncateToWidth(`${borderColor(BORDER_PREFIX)}${fittedContent}${BORDER_SUFFIX}${fill}`, width, "");
377
375
  }
378
376
 
379
377
  function renderToggleSegments(availableWidth: number): string {
@@ -396,5 +394,5 @@ function renderToggleSegments(availableWidth: number): string {
396
394
  }
397
395
 
398
396
  function stateThemeFg(color: "accent" | "muted", text: string): string {
399
- return state.theme?.fg(color, text) ?? text;
397
+ return state.theme?.().fg(color, text) ?? text;
400
398
  }
@@ -1,3 +1,4 @@
1
+ import { stripVTControlCharacters } from "node:util";
1
2
  import {
2
3
  DynamicBorder,
3
4
  getSettingsListTheme,
@@ -19,20 +20,23 @@ import {
19
20
  writeProjectSkillfulOverride,
20
21
  writeToggleSlots,
21
22
  } from "../config.js";
22
- import { replaceSkillsSection } from "../skill-prompt.js";
23
23
  import { isTopLevelSkill, listLoadedSkills, type LoadedSkillInfo } from "../skills.js";
24
24
  import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
25
25
  const SCOPES: SkillfulScope[] = ["global", "project"];
26
26
  const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
27
- const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV3");
27
+ // Install the new component contract even after reloading a V3-patched process.
28
+ const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV4");
28
29
 
29
30
  interface SkillVisibilityStore {
30
31
  hiddenSkillsByCwd: Map<string, Set<string>>;
31
32
  theme: Theme | null;
33
+ getTheme?: () => Theme;
32
34
  }
33
35
 
34
36
  interface ExpandableTextLike {
35
- getCollapsedText: () => string;
37
+ getCollapsedText?: () => string;
38
+ build?: () => string;
39
+ invalidate?: () => void;
36
40
  setText: (text: string) => void;
37
41
  }
38
42
 
@@ -43,6 +47,7 @@ interface BoxLike {
43
47
  interface InteractiveModeLike {
44
48
  loadedResourcesContainer?: BoxLike;
45
49
  showLoadedResources?: (options?: unknown) => void;
50
+ getStartupExpansionState?: () => boolean;
46
51
  session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
47
52
  sessionManager?: { getCwd?: () => string };
48
53
  }
@@ -84,6 +89,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
84
89
 
85
90
  pi.on("session_start", async (_event, ctx) => {
86
91
  store.theme = ctx.ui.theme;
92
+ store.getTheme = () => ctx.ui.theme;
87
93
  await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
88
94
  });
89
95
 
@@ -93,12 +99,9 @@ export default function skillVisibility(pi: ExtensionAPI) {
93
99
  const hidden = await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
94
100
  if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
95
101
 
96
- const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
102
+ event.systemPromptOptions.skills = event.systemPromptOptions.skills.map((skill) =>
97
103
  isTopLevelSkill(skill) && hidden.has(skill.name) ? { ...skill, disableModelInvocation: true } : skill,
98
104
  );
99
- const systemPrompt = replaceSkillsSection(event.systemPrompt, filteredSkills);
100
- if (!systemPrompt) return;
101
- return { systemPrompt };
102
105
  });
103
106
 
104
107
  pi.registerCommand("skillful", {
@@ -184,7 +187,6 @@ export function installStartupSkillListPatch(
184
187
  return result;
185
188
  };
186
189
 
187
- const childrenBefore = this.loadedResourcesContainer?.children.length ?? 0;
188
190
  try {
189
191
  original.call(this, options);
190
192
  } finally {
@@ -193,16 +195,36 @@ export function installStartupSkillListPatch(
193
195
 
194
196
  if (rawSkillNames.length === 0 || !cwd || !this.loadedResourcesContainer) return;
195
197
 
196
- const hidden = store.hiddenSkillsByCwd.get(cwd) ?? new Set<string>();
197
- const children = this.loadedResourcesContainer.children;
198
- for (let index = childrenBefore; index < children.length; index++) {
199
- const child = children[index] as ExpandableTextLike | undefined;
198
+ const colorized = () => buildColorizedSkillList(
199
+ rawSkillNames,
200
+ store.hiddenSkillsByCwd.get(cwd) ?? new Set<string>(),
201
+ store.getTheme?.() ?? store.theme,
202
+ );
203
+ const collapsedText = buildColorizedSkillList(rawSkillNames, new Set(), null);
204
+ // Pi clears and rebuilds this container, including during /reload.
205
+ for (const entry of this.loadedResourcesContainer.children) {
206
+ const child = entry as ExpandableTextLike | undefined;
207
+ if (child && typeof child.build === "function") {
208
+ // Pi 1.x closes over expansion state in ThemedText.build instead of
209
+ // exposing getCollapsedText. Decorate only the compact skill list;
210
+ // leave expanded paths and other sections to Pi. Rebuild colors on
211
+ // invalidation so theme changes never retain captured ANSI strings.
212
+ const build = child.build;
213
+ if (!stripVTControlCharacters(build.call(child)).startsWith("[Skills]\n")) continue;
214
+ child.build = () => {
215
+ const text = build.call(child);
216
+ return stripVTControlCharacters(text) === collapsedText ? colorized() : text;
217
+ };
218
+ child.invalidate?.();
219
+ break;
220
+ }
200
221
  if (!child || typeof child.getCollapsedText !== "function") continue;
201
222
  if (!child.getCollapsedText().includes("[Skills]")) continue;
202
223
 
203
- const colorized = buildColorizedSkillList(rawSkillNames, hidden, store.theme);
204
- child.getCollapsedText = () => colorized;
205
- child.setText(colorized);
224
+ // Retain the older Pi component contract without collapsing an
225
+ // initially expanded resource listing.
226
+ child.getCollapsedText = colorized;
227
+ if (!this.getStartupExpansionState?.()) child.setText(colorized());
206
228
  break;
207
229
  }
208
230
  };
@@ -1,12 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { fileURLToPath } from "node:url";
4
2
  import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { reportInstallTelemetry as report } from "@mocito/install-telemetry";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  const PACKAGE_NAME = "pi-skillful";
8
- const INSTALL_TELEMETRY_URL = "https://mocito.dev/api/report-install";
9
- const INSTALL_TELEMETRY_TIMEOUT_MS = 5000;
8
+ const INSTALL_TELEMETRY_ENDPOINT = "https://mocito.dev/api/report-install";
10
9
  const CI_ENVIRONMENT_VARIABLES = [
11
10
  "APPVEYOR",
12
11
  "BITBUCKET_BUILD_NUMBER",
@@ -24,10 +23,6 @@ const CI_ENVIRONMENT_VARIABLES = [
24
23
  "VERCEL",
25
24
  ];
26
25
 
27
- interface InstallTelemetryState {
28
- lastReportedVersion?: string;
29
- }
30
-
31
26
  interface PiSettingsDocument {
32
27
  enableInstallTelemetry?: unknown;
33
28
  }
@@ -51,18 +46,15 @@ function isPresentEnvFlag(value: string | undefined): boolean {
51
46
  return normalized !== "0" && normalized !== "false" && normalized !== "no";
52
47
  }
53
48
 
54
- function isCiEnvironment(): boolean {
55
- if (isTruthyEnvFlag(process.env.CI)) return true;
56
- return CI_ENVIRONMENT_VARIABLES.some((name) => isPresentEnvFlag(process.env[name]));
57
- }
58
-
59
- function isInstallTelemetryEnabled(): boolean {
60
- if (isCiEnvironment()) return false;
61
- if (isTruthyEnvFlag(process.env.PI_OFFLINE)) return false;
62
- if (process.env.PI_TELEMETRY !== undefined) return isTruthyEnvFlag(process.env.PI_TELEMETRY);
49
+ export function isInstallTelemetryEnabled(env: NodeJS.ProcessEnv = process.env, settingsPath = join(getAgentDir(), "settings.json")): boolean {
50
+ if (isTruthyEnvFlag(env.CI)) return false;
51
+ if (CI_ENVIRONMENT_VARIABLES.some((name) => isPresentEnvFlag(env[name]))) return false;
52
+ if (isTruthyEnvFlag(env.PI_OFFLINE)) return false;
63
53
 
64
- const settings = readJsonFile(join(getAgentDir(), "settings.json")) as PiSettingsDocument;
65
- return settings.enableInstallTelemetry !== false;
54
+ const settings = readJsonFile(settingsPath) as PiSettingsDocument;
55
+ if (settings.enableInstallTelemetry === false) return false;
56
+ if (env.PI_TELEMETRY !== undefined) return isTruthyEnvFlag(env.PI_TELEMETRY);
57
+ return true;
66
58
  }
67
59
 
68
60
  function getPackageVersion(): string {
@@ -70,35 +62,16 @@ function getPackageVersion(): string {
70
62
  return typeof packageJson.version === "string" && packageJson.version.length > 0 ? packageJson.version : "0.0.0";
71
63
  }
72
64
 
73
- function getInstallTelemetryUserAgent(version: string): string {
74
- const runtimeVersions = process.versions as NodeJS.ProcessVersions & { bun?: string };
75
- const runtime = runtimeVersions.bun ? `bun/${runtimeVersions.bun}` : `node/${process.version}`;
76
- return `${PACKAGE_NAME}/${version} (${process.platform}; ${runtime}; ${process.arch})`;
77
- }
78
-
79
- async function reportInstallTelemetryAsync(): Promise<void> {
65
+ export function reportInstallTelemetry(): void {
80
66
  try {
81
- if (!isInstallTelemetryEnabled()) return;
82
-
83
- const version = getPackageVersion();
84
- const extensionsDir = join(getAgentDir(), "extensions");
85
- const statePath = join(extensionsDir, "skillful-install.json");
86
- const state = readJsonFile(statePath) as InstallTelemetryState;
87
- if (state.lastReportedVersion === version) return;
88
-
89
- await mkdir(extensionsDir, { recursive: true });
90
- await writeFile(statePath, `${JSON.stringify({ lastReportedVersion: version }, null, 2)}\n`, "utf8");
91
-
92
- const params = new URLSearchParams({ tool: PACKAGE_NAME, version });
93
- await fetch(`${INSTALL_TELEMETRY_URL}?${params.toString()}`, {
94
- headers: { "User-Agent": getInstallTelemetryUserAgent(version) },
95
- signal: AbortSignal.timeout(INSTALL_TELEMETRY_TIMEOUT_MS),
96
- });
67
+ void report({
68
+ endpoint: INSTALL_TELEMETRY_ENDPOINT,
69
+ tool: PACKAGE_NAME,
70
+ version: getPackageVersion(),
71
+ statePath: join(getAgentDir(), "extensions", "skillful-install.json"),
72
+ enabled: isInstallTelemetryEnabled(),
73
+ }).catch(() => undefined);
97
74
  } catch {
98
- // Best-effort telemetry: ignore settings, filesystem, and network failures.
75
+ // Best-effort telemetry: ignore local policy and filesystem failures.
99
76
  }
100
77
  }
101
-
102
- export function reportInstallTelemetry(): void {
103
- void reportInstallTelemetryAsync();
104
- }
@@ -1,8 +0,0 @@
1
- import { formatSkillsForPrompt, type Skill } from "@earendil-works/pi-coding-agent";
2
-
3
- export const SKILLS_SECTION_PATTERN = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
4
-
5
- export function replaceSkillsSection(systemPrompt: string, skills: Skill[]): string | undefined {
6
- const next = systemPrompt.replace(SKILLS_SECTION_PATTERN, formatSkillsForPrompt(skills));
7
- return next === systemPrompt ? undefined : next;
8
- }