@aliou/pi-dev-kit 0.6.4 → 0.7.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.
@@ -1,306 +1,389 @@
1
1
  # Additional APIs
2
2
 
3
- These APIs are available on `ExtensionAPI` and `ExtensionContext` but are less commonly used. Each is shown with a minimal example.
4
-
5
- When you implement something using one of these APIs, update this skill reference with a fuller example based on your actual usage.
3
+ This reference covers less common `ExtensionAPI`, `ExtensionContext`, and `ExtensionCommandContext` APIs.
6
4
 
7
5
  ## Shortcuts
8
6
 
9
- Register global keyboard shortcuts:
7
+ Register keyboard shortcuts for interactive mode.
10
8
 
11
9
  ```typescript
12
10
  pi.registerShortcut("ctrl+shift+p", {
13
11
  description: "Toggle plan mode",
14
12
  handler: async (ctx) => {
15
13
  planModeEnabled = !planModeEnabled;
16
- ctx.ui.setStatus("plan", planModeEnabled ? "Plan Mode" : "");
14
+ ctx.ui.setStatus("plan-mode", planModeEnabled ? ctx.ui.theme.fg("accent", "Plan") : undefined);
17
15
  },
18
16
  });
19
17
  ```
20
18
 
21
- Shortcuts work only in Interactive mode.
19
+ Shortcuts are TUI-only.
22
20
 
23
21
  ## Flags
24
22
 
25
- Register boolean flags that persist across sessions:
23
+ Register CLI flags and read them in any handler.
26
24
 
27
25
  ```typescript
28
- // Register
29
- pi.registerFlag("auto-commit", {
30
- description: "Auto-commit after each turn",
26
+ pi.registerFlag("plan", {
27
+ description: "Start in plan mode",
28
+ type: "boolean",
31
29
  default: false,
32
30
  });
33
31
 
34
- // Read (in any handler)
35
- const autoCommit = pi.getFlag("auto-commit");
32
+ const planEnabled = pi.getFlag("plan") === true;
36
33
  ```
37
34
 
38
- Users toggle flags with `/flag auto-commit` in the input editor.
35
+ ## Commands and Sessions
39
36
 
40
- ## sendUserMessage
37
+ Command handlers receive `ExtensionCommandContext`, which adds session-control methods. These methods are command-only because they can deadlock from event handlers.
41
38
 
42
- Inject a user message into the conversation programmatically:
39
+ ### Wait for idle
43
40
 
44
41
  ```typescript
45
- pi.sendUserMessage("Please summarize what we just discussed");
42
+ pi.registerCommand("safe-command", {
43
+ handler: async (_args, ctx) => {
44
+ await ctx.waitForIdle();
45
+ // Safe to inspect or replace session state.
46
+ },
47
+ });
46
48
  ```
47
49
 
48
- This triggers a full agent turn as if the user typed the message. Useful for file watchers, timers, or other automated triggers.
50
+ ### New, fork, switch
49
51
 
50
- ## Session Name
51
-
52
- Set or get a name for the current session (shown in the session selector):
52
+ Use `withSession` for post-replacement work. Captured old `pi`, old command `ctx`, and old `ctx.sessionManager` are stale after replacement.
53
53
 
54
54
  ```typescript
55
- pi.setSessionName("Feature: Auth Refactor");
56
- const name = pi.getSessionName();
55
+ await ctx.newSession({
56
+ setup: async (sm) => {
57
+ sm.appendMessage({
58
+ role: "user",
59
+ content: [{ type: "text", text: "Seed context" }],
60
+ timestamp: Date.now(),
61
+ });
62
+ },
63
+ withSession: async (ctx) => {
64
+ await ctx.sendUserMessage("Continue from the new session");
65
+ },
66
+ });
57
67
  ```
58
68
 
59
- ## Labels
69
+ ```typescript
70
+ await ctx.fork(entryId, {
71
+ position: "at",
72
+ withSession: async (ctx) => ctx.ui.notify("Forked", "info"),
73
+ });
74
+
75
+ await ctx.switchSession(sessionPath, {
76
+ withSession: async (ctx) => {
77
+ await ctx.sendUserMessage("Resume this work here");
78
+ },
79
+ });
80
+ ```
60
81
 
61
- Set a label on a specific session entry (shown in `/tree` view):
82
+ ### Tree navigation
62
83
 
63
84
  ```typescript
64
- pi.setLabel(entryId, "checkpoint: before refactor");
85
+ await ctx.navigateTree(targetId, {
86
+ summarize: true,
87
+ customInstructions: "Focus on implementation decisions",
88
+ replaceInstructions: false,
89
+ label: "review-checkpoint",
90
+ });
65
91
  ```
66
92
 
67
- ## exec
93
+ ## Reload
68
94
 
69
- Run a shell command and get the result. This is the **only** way to run external binaries or shell scripts from an extension.
95
+ Use `ctx.reload()` in command handlers. Treat it as terminal for predictable behavior.
70
96
 
71
97
  ```typescript
72
- const result = await pi.exec("git status --porcelain", { cwd: process.cwd() });
73
- // result: { stdout, stderr, exitCode }
98
+ pi.registerCommand("reload-runtime", {
99
+ description: "Reload extensions, skills, prompts, and themes",
100
+ handler: async (_args, ctx) => {
101
+ await ctx.reload();
102
+ return;
103
+ },
104
+ });
74
105
  ```
75
106
 
76
- Useful for git operations, environment checks, running CLI tools, etc.
107
+ Code after `await ctx.reload()` still runs in the old call frame. Avoid post-reload work in that handler.
77
108
 
78
- **Do not use Node `child_process` APIs** (`exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, `execFileSync`). `pi.exec` handles CWD resolution, output capture, and integrates with the extension lifecycle. Using `child_process` directly bypasses these guarantees and creates inconsistent behavior across environments.
109
+ Tools cannot call `ctx.reload()`. If the LLM needs a reload tool, create a tool that queues a reload command as a follow-up user message.
79
110
 
80
- The only exception is a long-lived streaming process that requires direct stdin/stdout piping — document the reason in code comments if this applies.
111
+ ```typescript
112
+ async execute() {
113
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
114
+ return { content: [{ type: "text", text: "Queued /reload-runtime." }] };
115
+ }
116
+ ```
81
117
 
82
- ## Active Tools
118
+ ## Sending Messages
119
+
120
+ ### `pi.sendUserMessage(content, options?)`
83
121
 
84
- Get or set which tools are currently active:
122
+ Sends an actual user message and triggers a turn.
85
123
 
86
124
  ```typescript
87
- const tools = pi.getActiveTools(); // string[]
88
- pi.setActiveTools(["bash", "read", "write", "my_custom_tool"]);
125
+ pi.sendUserMessage("Summarize the current state.");
126
+ pi.sendUserMessage("Focus on tests next.", { deliverAs: "steer" });
127
+ pi.sendUserMessage("After that, summarize.", { deliverAs: "followUp" });
89
128
  ```
90
129
 
91
- Setting active tools restricts which tools the LLM can use.
130
+ When the agent is streaming, specify `deliverAs: "steer"` or `"followUp"`.
92
131
 
93
- ## Model Control
132
+ ### `pi.sendMessage(message, options?)`
133
+
134
+ Sends a custom message. Use `registerMessageRenderer` for custom display.
94
135
 
95
136
  ```typescript
96
- // Set the active model
97
- pi.setModel("anthropic/claude-sonnet-4-20250514");
137
+ pi.sendMessage(
138
+ {
139
+ customType: "my-extension-status",
140
+ content: "Status update",
141
+ display: true,
142
+ details: { status: "ok" },
143
+ },
144
+ { deliverAs: "nextTurn" },
145
+ );
146
+ ```
98
147
 
99
- // Get/set thinking level
100
- const level = pi.getThinkingLevel(); // "none" | "low" | "medium" | "high"
101
- pi.setThinkingLevel("high");
148
+ Delivery modes:
149
+
150
+ - `steer`: queue while streaming and deliver before next LLM call.
151
+ - `followUp`: wait until the agent finishes.
152
+ - `nextTurn`: save for the next user prompt.
153
+
154
+ ## State and Session Metadata
155
+
156
+ ```typescript
157
+ pi.appendEntry("my-extension-state", { enabled: true });
158
+ pi.setSessionName("Feature: auth refactor");
159
+ const name = pi.getSessionName();
160
+ pi.setLabel(entryId, "checkpoint-before-refactor");
161
+ pi.setLabel(entryId, undefined); // clear
102
162
  ```
103
163
 
104
- ## System Prompt
164
+ Use labels for `/tree` bookmarks and session names for the session selector.
105
165
 
106
- Read or modify the system prompt (typically in `before_agent_start`):
166
+ ## Shell Execution
167
+
168
+ Use `pi.exec(command, args, options?)` for shell commands.
107
169
 
108
170
  ```typescript
109
- pi.on("before_agent_start", async (_event, ctx) => {
110
- const prompt = ctx.getSystemPrompt();
111
- ctx.setSystemPrompt(prompt + "\n\nExtra instructions.");
171
+ const result = await pi.exec("git", ["status", "--porcelain"], {
172
+ cwd: ctx.cwd,
173
+ signal,
174
+ timeout: 5_000,
112
175
  });
176
+
177
+ // result.stdout, result.stderr, result.code, result.killed
113
178
  ```
114
179
 
115
- The system prompt resets each turn, so modifications are not cumulative.
180
+ Do not use Node `child_process` APIs for normal command execution. The only exception is a documented long-lived streaming process with direct stdin/stdout needs that `pi.exec()` cannot support.
116
181
 
117
- ### Guidance Injection Pattern
182
+ ## Active Tools
183
+
184
+ ```typescript
185
+ const active = pi.getActiveTools();
186
+ const all = pi.getAllTools();
187
+ const builtin = all.filter((tool) => tool.sourceInfo.source === "builtin");
188
+
189
+ pi.setActiveTools(["read", "grep", "find"]);
190
+ ```
118
191
 
119
- Extensions that add tools or behavioral patterns the agent may not know how to use correctly should inject guidance into the system prompt. Without it, agents fall back to bash workarounds even when a better tool is available.
192
+ `pi.getAllTools()` includes built-in tools, SDK tools, and extension tools with `sourceInfo` provenance.
120
193
 
121
- **When to inject guidance:**
122
- - Your extension adds a tool that competes with a natural bash fallback (e.g. a process manager, a CI watcher, a search tool)
123
- - Correct usage depends on subtle conditions (alert flags, when-not-to-use, alert vs. poll)
124
- - You have observed agents ignoring the tool or reaching for `bash` instead
194
+ ## Model and Thinking Control
125
195
 
126
- **When not to:**
127
- - The tool description alone is self-explanatory
128
- - The tool has no plausible bash alternative
196
+ ```typescript
197
+ const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
198
+ if (model) {
199
+ const success = await pi.setModel(model);
200
+ if (!success) ctx.ui.notify("No API key for this model", "error");
201
+ }
129
202
 
130
- ---
203
+ const level = pi.getThinkingLevel();
204
+ pi.setThinkingLevel("high");
205
+ ```
131
206
 
132
- There are two ways to inject guidance, depending on complexity:
207
+ Thinking levels are `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, and `"xhigh"`. Pi clamps unsupported levels to model capabilities.
133
208
 
134
- #### Tier 1: Per-Tool Metadata (Preferred for Simple Tools)
209
+ ## System Prompt Guidance
135
210
 
136
- For most tools, use the SDK-level `promptSnippet` and `promptGuidelines` fields directly on the tool definition. No hook is needed.
211
+ Prefer tool-level `promptSnippet` and `promptGuidelines` for simple guidance. Use `before_agent_start` only for dynamic or cross-tool guidance.
137
212
 
138
- - **`promptSnippet`** — Injected into the "Available tools" system prompt section. Use for a concise (1–2 sentence) description of when to prefer this tool.
139
- - **`promptGuidelines`** — Appended verbatim to the global "Guidelines" section. Use for a short list of usage rules that still make sense without extra tool-local context.
213
+ ### Per-tool metadata
140
214
 
141
215
  ```typescript
142
- const myTool = {
216
+ const myTool = defineTool({
143
217
  name: "my_tool",
144
218
  label: "My Tool",
145
219
  description: "...",
146
- promptSnippet: "Manage background processes without blocking the conversation.",
220
+ promptSnippet: "Manage background work without blocking the conversation.",
147
221
  promptGuidelines: [
148
- "Use my_tool for long-running commands instead of bash.",
149
- "After starting my_tool, continue other work instead of waiting.",
222
+ "Use my_tool for long-running commands instead of shell backgrounding.",
223
+ "After starting my_tool, continue useful work instead of polling my_tool immediately.",
150
224
  ],
151
- parameters: ...,
152
- execute: ...,
153
- };
225
+ parameters,
226
+ async execute() {
227
+ // ...
228
+ },
229
+ });
154
230
  ```
155
231
 
156
- This is the simplest approach and works well when guidance is specific to a single tool.
157
-
158
- Because these bullets are merged into the shared global `Guidelines` section, avoid vague phrasing like `Use this tool...`. Name the exact tool (`my_tool`, `process`, `linkup_web_search`) so the bullet remains clear after injection.
159
-
160
- #### Tier 2: System Prompt Hook (For Complex Cross-Tool Orchestration)
232
+ Every `promptGuidelines` bullet must name the exact tool because Pi injects bullets flat into the global Guidelines section.
161
233
 
162
- Use the `before_agent_start` hook when:
163
- - Guidance involves **cross-tool workflow instructions** (e.g. "use tool A, then tool B, then tool C")
164
- - You need **dynamic context from config** (e.g. workspace names, team keys, feature flags)
165
- - The per-tool metadata fields aren't expressive enough
166
-
167
- **Structure: three files**
168
-
169
- `src/guidance.ts` — the guidance text as a named export:
234
+ ### System prompt hook
170
235
 
171
236
  ```typescript
172
237
  export const MY_EXTENSION_GUIDANCE = `
173
238
  ## My Extension
174
239
 
175
- Use the \`my_tool\` tool for X. Don't use bash for X.
240
+ Use \`my_tool\` when ...
241
+ Do not use bash workaround ...
242
+ `;
176
243
 
177
- **Use \`my_tool\` when:**
178
- - Situation A
179
- - Situation B
244
+ pi.on("before_agent_start", async (event) => {
245
+ if (!configLoader.getConfig().systemPromptGuidance) return;
246
+ return {
247
+ systemPrompt: `${event.systemPrompt}\n\n${MY_EXTENSION_GUIDANCE}`,
248
+ };
249
+ });
250
+ ```
180
251
 
181
- **Use \`bash\` when:**
182
- - You need the result immediately to proceed (quick commands that finish in seconds)
252
+ Use `event.systemPromptOptions` when you need structured prompt inputs such as selected tools, loaded skills, context files, and accumulated prompt guidelines.
183
253
 
184
- **Never do this:**
185
- \`\`\`bash
186
- workaround_command # loses observability
187
- \`\`\`
254
+ ## Compaction and Shutdown
188
255
 
189
- **Do this instead:**
190
- \`\`\`
191
- my_tool({ action: "start", ... })
192
- \`\`\`
193
- `;
256
+ Use `ctx.compact()` to trigger compaction without awaiting the full operation.
257
+
258
+ ```typescript
259
+ ctx.compact({
260
+ customInstructions: "Focus on recent code changes",
261
+ onComplete: (result) => ctx.ui.notify("Compaction complete", "info"),
262
+ onError: (error) => ctx.ui.notify(`Compaction failed: ${error.message}`, "error"),
263
+ });
194
264
  ```
195
265
 
196
- `src/hooks/system-prompt.ts` the hook:
266
+ Use `ctx.shutdown()` to request graceful shutdown.
197
267
 
198
268
  ```typescript
199
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
200
- import { configLoader } from "../config";
201
- import { MY_EXTENSION_GUIDANCE } from "../guidance";
269
+ ctx.shutdown();
270
+ ```
202
271
 
203
- export function registerGuidance(pi: ExtensionAPI): void {
204
- pi.on("before_agent_start", async (event) => {
205
- const config = configLoader.getConfig();
206
- if (!config.systemPromptGuidance) return;
272
+ In interactive and RPC modes, shutdown is deferred until Pi becomes idle. In print mode it is a no-op.
207
273
 
208
- return {
209
- systemPrompt: `${event.systemPrompt}\n${MY_EXTENSION_GUIDANCE}`,
210
- };
211
- });
212
- }
213
- ```
274
+ ## Event Bus
214
275
 
215
- `src/config.ts` add the toggle (default `true`):
276
+ Use the shared event bus only when extensions need to coordinate.
216
277
 
217
278
  ```typescript
218
- export interface MyExtensionConfig {
219
- // ...
220
- /** Inject tool guidance into the system prompt each turn. Default: true. */
221
- systemPromptGuidance?: boolean;
222
- }
279
+ pi.events.emit("my-extension:data-ready", { items });
280
+ pi.events.on("my-extension:data-ready", (data) => {
281
+ console.log(data.items.length);
282
+ });
223
283
  ```
224
284
 
225
- Call `registerGuidance(pi)` from your hooks setup function.
285
+ Namespace event names with your extension name.
226
286
 
227
- ---
287
+ ## UI Customization
228
288
 
229
- **What makes guidance effective:**
289
+ ### Working indicator and message
230
290
 
231
- - Lead with the decision rule: **when to use AND when not to use**. The when-not-to-use is as important — it gives the agent permission to keep using `bash` for quick tasks and prevents overcorrection.
232
- - Name anti-patterns explicitly by their exact form (`cmd &`, `nohup`, `sleep 30 &&`). Abstract descriptions ("don't use bash workarounds") are ignored.
233
- - Use 2–3 tight code examples. More than that dilutes attention; fewer leave the pattern underspecified.
234
- - Keep the guidance section header (`## My Extension`) so it reads as a named capability, not a restriction.
235
- - Avoid stacking emphasis markers (`NEVER`, `ALWAYS`, `IMPORTANT`). One or two land; more are ignored.
291
+ ```typescript
292
+ ctx.ui.setWorkingMessage("Thinking through plan...");
293
+ ctx.ui.setWorkingMessage(); // restore default
236
294
 
237
- **Reference implementations:**
238
- - `pi-linkup` — Uses per-tool `promptSnippet`/`promptGuidelines` (simple tools, no hook needed).
239
- - `pi-linear` — Uses `guidance.ts` + `before_agent_start` hook (cross-tool workflow instructions + dynamic workspace context).
240
- - `pi-processes` — Uses both: `promptSnippet`/`promptGuidelines` on tools for basic guidance, plus system prompt hook for complex multi-tool orchestration patterns.
295
+ ctx.ui.setWorkingVisible(false);
296
+ ctx.ui.setWorkingVisible(true);
241
297
 
242
- ## Compaction
298
+ ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
299
+ ctx.ui.setWorkingIndicator({ frames: [] }); // hide indicator
300
+ ctx.ui.setWorkingIndicator(); // restore default
301
+ ```
243
302
 
244
- Trigger compaction programmatically:
303
+ ### Widgets, title, editor text
245
304
 
246
305
  ```typescript
247
- await pi.compact();
306
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
307
+ ctx.ui.setWidget("my-widget", ["Below"], { placement: "belowEditor" });
308
+ ctx.ui.setWidget("my-widget", undefined);
309
+
310
+ ctx.ui.setTitle("pi - my project");
311
+ ctx.ui.setEditorText("Prefilled prompt");
312
+ const editorText = ctx.ui.getEditorText();
313
+ ctx.ui.pasteToEditor("Pasted content");
248
314
  ```
249
315
 
250
- ## Shutdown
251
-
252
- Shut down pi gracefully:
316
+ ### Footer
253
317
 
254
318
  ```typescript
255
- pi.shutdown();
319
+ ctx.ui.setFooter((tui, theme, footerData) => ({
320
+ invalidate() {},
321
+ render(width) {
322
+ return [theme.fg("dim", footerData.getGitBranch() ?? "no git")];
323
+ },
324
+ dispose: footerData.onBranchChange(() => tui.requestRender()),
325
+ }));
326
+
327
+ ctx.ui.setFooter(undefined);
256
328
  ```
257
329
 
258
- ## EventBus
330
+ ### Autocomplete providers
259
331
 
260
- Inter-extension communication via a shared event bus:
332
+ Stack on top of the current provider and delegate when your syntax does not match.
261
333
 
262
334
  ```typescript
263
- // Extension A: emit
264
- pi.events.emit("my-extension:data-ready", { items: [...] });
335
+ ctx.ui.addAutocompleteProvider((current) => ({
336
+ async getSuggestions(lines, cursorLine, cursorCol, options) {
337
+ const beforeCursor = (lines[cursorLine] ?? "").slice(0, cursorCol);
338
+ const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
339
+ if (!match) return current.getSuggestions(lines, cursorLine, cursorCol, options);
265
340
 
266
- // Extension B: listen
267
- pi.events.on("my-extension:data-ready", (data) => {
268
- console.log("Received:", data.items.length, "items");
269
- });
341
+ return {
342
+ prefix: `#${match[1] ?? ""}`,
343
+ items: [{ value: "#123", label: "#123", description: "Issue title" }],
344
+ };
345
+ },
346
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
347
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
348
+ },
349
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
350
+ return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
351
+ },
352
+ }));
270
353
  ```
271
354
 
272
- Namespace event names with your extension name to avoid collisions. The event bus is supplementary -- most extensions do not need it. Use it when two extensions need to coordinate.
273
-
274
- ## Theme Control
355
+ ### Theme control
275
356
 
276
357
  ```typescript
277
- // Get current and available themes
278
- const current = ctx.ui.getTheme();
279
- const all = ctx.ui.getAllThemes();
280
-
281
- // Set theme
282
- const result = ctx.ui.setTheme("catppuccin-mocha");
283
- // result: { success: boolean, error?: string }
358
+ const themes = ctx.ui.getAllThemes();
359
+ const light = ctx.ui.getTheme("light");
360
+ const result = ctx.ui.setTheme("light");
361
+ if (!result.success) ctx.ui.notify(result.error, "error");
284
362
  ```
285
363
 
286
- ## UI Customization
364
+ ## Pi Paths
287
365
 
288
- ```typescript
289
- // Replace the footer
290
- ctx.ui.setFooter((maxWidth, theme) => {
291
- return theme.fg("muted", "Custom footer content");
292
- });
366
+ Use SDK helpers for Pi paths instead of `homedir()` when helpers exist. They respect `PI_CODING_AGENT_DIR` and test/custom setups.
293
367
 
294
- // Replace the startup header
295
- ctx.ui.setHeader((maxWidth, theme) => {
296
- return theme.fg("accent", "My Custom Header");
297
- });
368
+ Common helpers exported from the main package include:
298
369
 
299
- // Set the editor component
300
- ctx.ui.setEditorComponent((tui, theme, kb) => {
301
- return new CustomEditor(tui, theme, kb);
302
- });
370
+ - `getAgentDir()`
371
+ - `getSettingsPath()`
372
+ - `getSessionsDir()`
373
+ - `getPromptsDir()`
374
+ - `getToolsDir()`
375
+ - `getCustomThemesDir()`
376
+ - `getModelsPath()`
377
+ - `getAuthPath()`
378
+ - `getBinDir()`
379
+ - `getDebugLogPath()`
303
380
 
304
- // Prefill the editor
305
- ctx.ui.setEditorText("Prefilled content");
306
- ```
381
+ ## Checklist
382
+
383
+ - [ ] Session replacement code uses `withSession` for post-switch work.
384
+ - [ ] Reload handlers return immediately after `await ctx.reload()`.
385
+ - [ ] `pi.exec()` is used instead of `child_process`.
386
+ - [ ] System prompt changes return `{ systemPrompt }` from `before_agent_start`.
387
+ - [ ] `promptGuidelines` bullets name exact tools.
388
+ - [ ] UI customizations account for RPC/print degradation.
389
+ - [ ] Pi path helpers are used instead of `homedir()`.