@counterposition/skill-pi 0.1.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.
@@ -0,0 +1,97 @@
1
+ # Pi Packages
2
+
3
+ Packages bundle extensions, skills, prompt templates, and themes for sharing via npm, git, or local paths.
4
+
5
+ **Security warning:** Packages run with full system access. Extensions execute arbitrary code, skills can instruct the model to perform any action. Review source before installing third-party packages.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # From npm
11
+ pi install npm:@foo/pi-extensions@1.0.0
12
+
13
+ # From git
14
+ pi install git:github.com/user/repo@v1
15
+ pi install https://github.com/user/repo.git
16
+ pi install git@github.com:user/repo.git
17
+
18
+ # From local path
19
+ pi install ./my-package
20
+ pi install /absolute/path/to/package
21
+
22
+ # Project-local install (writes to .pi/settings.json for team sharing)
23
+ pi install -l npm:@foo/pi-extensions
24
+
25
+ # Temporary trial (loads for current session, doesn't install)
26
+ pi -e npm:@foo/pi-extensions
27
+ ```
28
+
29
+ ## Management Commands
30
+
31
+ ```bash
32
+ pi list # List installed packages
33
+ pi update # Update non-pinned packages
34
+ pi remove <pkg> # Remove a package
35
+ ```
36
+
37
+ ## Package Structure
38
+
39
+ ### Convention-Based (Auto-Discovery)
40
+
41
+ Place files in standard directories — Pi discovers them automatically:
42
+
43
+ ```text
44
+ my-pi-package/
45
+ ├── package.json # npm package metadata + optional pi manifest
46
+ ├── extensions/ # .ts and .js files → loaded as extensions
47
+ │ └── my-ext.ts
48
+ ├── skills/ # Directories with SKILL.md → loaded as skills
49
+ │ └── my-skill/
50
+ │ └── SKILL.md
51
+ ├── prompts/ # .md files → loaded as prompt templates
52
+ │ └── review.md
53
+ └── themes/ # .json files → loaded as themes
54
+ └── my-theme.json
55
+ ```
56
+
57
+ ### Manifest-Based (Explicit)
58
+
59
+ Declare resources in `package.json` under the `pi` key:
60
+
61
+ ```json
62
+ {
63
+ "name": "@foo/pi-extensions",
64
+ "version": "1.0.0",
65
+ "pi": {
66
+ "extensions": ["src/ext1.ts", "src/ext2.ts"],
67
+ "skills": ["skills/my-skill"],
68
+ "prompts": ["prompts/"],
69
+ "themes": ["themes/custom.json"]
70
+ }
71
+ }
72
+ ```
73
+
74
+ Both approaches can be combined. Explicit paths in the `pi` manifest take precedence.
75
+
76
+ ## Dependencies
77
+
78
+ - **Standard npm dependencies:** Declare in `package.json` → `dependencies` as usual. They're installed with the package.
79
+ - **Pi peer dependencies** (`@mariozechner/pi-coding-agent`, `@mariozechner/pi-ai`, etc.): Use `"*"` as the version range in `peerDependencies`. Do NOT bundle these.
80
+ - **Other Pi packages:** If your package depends on another Pi package, it must be bundled explicitly.
81
+
82
+ ## Publishing to npm
83
+
84
+ ```bash
85
+ cd my-pi-package
86
+ npm publish
87
+ ```
88
+
89
+ Users install with `pi install npm:@foo/pi-extensions`.
90
+
91
+ ## Deduplication
92
+
93
+ When the same resource name appears from multiple sources:
94
+
95
+ - Project-level packages override global ones
96
+ - First discovery wins for same-level conflicts
97
+ - Pi warns about collisions
@@ -0,0 +1,314 @@
1
+ # Common Patterns & Recipes
2
+
3
+ Practical recipes for extending Pi. Each pattern is a self-contained example.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Permission Gate](#permission-gate)
8
+ 2. [Protected Paths](#protected-paths)
9
+ 3. [Preset System](#preset-system)
10
+ 4. [Sub-agents](#sub-agents)
11
+ 5. [Git Checkpoint & Restore](#git-checkpoint--restore)
12
+ 6. [Auto-Commit on Exit](#auto-commit-on-exit)
13
+ 7. [Custom Compaction](#custom-compaction)
14
+ 8. [Plan Mode (Read-Only)](#plan-mode-read-only)
15
+ 9. [Claude Rules Integration](#claude-rules-integration)
16
+ 10. [Interactive Shell](#interactive-shell)
17
+ 11. [TUI Components](#tui-components)
18
+ 12. [Status Line & Widgets](#status-line--widgets)
19
+ 13. [File Watcher / Trigger](#file-watcher--trigger)
20
+ 14. [Input Transform](#input-transform)
21
+ 15. [Tool Override](#tool-override)
22
+
23
+ ---
24
+
25
+ ## Permission Gate
26
+
27
+ Block dangerous bash commands with user confirmation:
28
+
29
+ ```typescript
30
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
31
+
32
+ export default function (pi: ExtensionAPI) {
33
+ const dangerous = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
34
+
35
+ pi.on("tool_call", async (event, ctx) => {
36
+ if (event.toolName !== "bash") return;
37
+ const cmd = event.input.command as string;
38
+ if (!dangerous.some(p => p.test(cmd))) return;
39
+ if (!ctx.hasUI) return { block: true, reason: "Dangerous command blocked (no UI)" };
40
+ const ok = await ctx.ui.select(`⚠️ ${cmd}\nAllow?`, ["Yes", "No"]);
41
+ if (ok !== "Yes") return { block: true, reason: "Blocked by user" };
42
+ });
43
+ }
44
+ ```
45
+
46
+ ## Protected Paths
47
+
48
+ Block writes to sensitive files:
49
+
50
+ ```typescript
51
+ export default function (pi: ExtensionAPI) {
52
+ const protected = [".env", ".git/", "node_modules/", "*.pem", "*.key"];
53
+
54
+ pi.on("tool_call", async (event, ctx) => {
55
+ if (!["write", "edit"].includes(event.toolName)) return;
56
+ const path = (event.input.file_path || event.input.path) as string;
57
+ if (protected.some(p => path.includes(p))) {
58
+ return { block: true, reason: `Protected path: ${path}` };
59
+ }
60
+ });
61
+ }
62
+ ```
63
+
64
+ ## Preset System
65
+
66
+ Named configurations (model + thinking + tools + instructions):
67
+
68
+ **`~/.pi/agent/presets.json`:**
69
+
70
+ ```json
71
+ {
72
+ "plan": {
73
+ "model": "claude-sonnet-4-20250514",
74
+ "thinkingLevel": "high",
75
+ "tools": ["read", "grep", "find", "ls"],
76
+ "instructions": "Analyze the codebase and propose a plan. Do not modify any files."
77
+ },
78
+ "implement": {
79
+ "model": "claude-sonnet-4-20250514",
80
+ "thinkingLevel": "medium",
81
+ "tools": ["read", "bash", "edit", "write"],
82
+ "instructions": "Implement the requested changes with minimal, focused edits."
83
+ }
84
+ }
85
+ ```
86
+
87
+ The preset extension (from Pi examples) loads these and exposes `/preset [name]` + `Ctrl+Shift+U` cycling.
88
+
89
+ ## Sub-agents
90
+
91
+ Delegate tasks to isolated Pi processes. The subagent extension (from Pi examples) provides:
92
+
93
+ ```text
94
+ // Single agent
95
+ { agent: "researcher", task: "Find all API endpoints in this codebase" }
96
+
97
+ // Parallel execution
98
+ { tasks: [
99
+ { agent: "researcher", task: "Analyze auth module" },
100
+ { agent: "researcher", task: "Analyze database module" }
101
+ ]}
102
+
103
+ // Sequential chain with context passing
104
+ { chain: [
105
+ { agent: "researcher", task: "Find all TODO comments" },
106
+ { agent: "writer", task: "Write a summary of: {previous}" }
107
+ ]}
108
+ ```
109
+
110
+ Agent definitions live in `~/.pi/agent/agents/` or `.pi/agents/`. Each is a directory with a config file specifying model, tools, and system prompt.
111
+
112
+ ## Git Checkpoint & Restore
113
+
114
+ Save code state at each turn, offer restore on fork:
115
+
116
+ ```typescript
117
+ export default function (pi: ExtensionAPI) {
118
+ const checkpoints = new Map<string, string>();
119
+ let currentEntryId: string | undefined;
120
+
121
+ pi.on("tool_result", async (_event, ctx) => {
122
+ const leaf = ctx.sessionManager.getLeafEntry();
123
+ if (leaf) currentEntryId = leaf.id;
124
+ });
125
+
126
+ pi.on("turn_start", async () => {
127
+ const { stdout } = await pi.exec("git", ["stash", "create"]);
128
+ if (stdout.trim() && currentEntryId) checkpoints.set(currentEntryId, stdout.trim());
129
+ });
130
+
131
+ pi.on("session_before_fork", async (event, ctx) => {
132
+ const ref = checkpoints.get(event.entryId);
133
+ if (!ref || !ctx.hasUI) return;
134
+ const ok = await ctx.ui.select("Restore code?", ["Yes", "No"]);
135
+ if (ok?.startsWith("Yes")) await pi.exec("git", ["stash", "apply", ref]);
136
+ });
137
+ }
138
+ ```
139
+
140
+ ## Auto-Commit on Exit
141
+
142
+ Stage and commit all changes when session ends:
143
+
144
+ ```typescript
145
+ export default function (pi: ExtensionAPI) {
146
+ pi.on("session_shutdown", async (_event, ctx) => {
147
+ const { stdout } = await pi.exec("git", ["status", "--porcelain"]);
148
+ if (!stdout.trim()) return;
149
+
150
+ const messages = ctx.sessionManager.getBranch();
151
+ const lastAssistant = messages.reverse().find(m => m.role === "assistant");
152
+ const text = lastAssistant?.content?.find(c => c.type === "text")?.text || "pi session changes";
153
+ const firstLine = text.split("\n")[0].slice(0, 50);
154
+
155
+ await pi.exec("git", ["add", "-A"]);
156
+ await pi.exec("git", ["commit", "-m", `[pi] ${firstLine}`]);
157
+ ctx.ui?.notify("Changes committed", "info");
158
+ });
159
+ }
160
+ ```
161
+
162
+ ## Custom Compaction
163
+
164
+ Replace default compaction with a custom summary model:
165
+
166
+ ```typescript
167
+ export default function (pi: ExtensionAPI) {
168
+ pi.on("session_before_compact", async (event, ctx) => {
169
+ const allMessages = [...event.messagesToSummarize, ...event.turnPrefixMessages];
170
+ // Use a cheaper model for summarization
171
+ const summary = await myCustomSummarize(allMessages);
172
+ return {
173
+ summary,
174
+ firstKeptEntryId: event.firstKeptEntryId,
175
+ tokensBefore: event.tokens,
176
+ };
177
+ });
178
+ }
179
+ ```
180
+
181
+ ## Plan Mode (Read-Only)
182
+
183
+ Restrict to read-only tools for safe exploration:
184
+
185
+ ```typescript
186
+ export default function (pi: ExtensionAPI) {
187
+ let planMode = false;
188
+
189
+ pi.registerCommand("plan", {
190
+ description: "Toggle read-only plan mode",
191
+ async handler(_args, ctx) {
192
+ planMode = !planMode;
193
+ if (planMode) {
194
+ pi.setActiveTools(["read", "bash", "grep", "find", "ls"]);
195
+ ctx.ui.notify("Plan mode ON — read-only tools only", "info");
196
+ } else {
197
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
198
+ ctx.ui.notify("Plan mode OFF — full tools restored", "info");
199
+ }
200
+ },
201
+ });
202
+
203
+ pi.on("tool_call", async (event) => {
204
+ if (!planMode || event.toolName !== "bash") return;
205
+ const cmd = event.input.command as string;
206
+ // Block destructive bash in plan mode
207
+ if (/\b(rm|mv|cp|chmod|chown|git\s+(push|commit|reset))\b/.test(cmd)) {
208
+ return { block: true, reason: "Blocked in plan mode" };
209
+ }
210
+ });
211
+ }
212
+ ```
213
+
214
+ ## Claude Rules Integration
215
+
216
+ Scan `.claude/rules/` and inject into system prompt:
217
+
218
+ ```typescript
219
+ export default function (pi: ExtensionAPI) {
220
+ pi.on("before_agent_start", async (event, ctx) => {
221
+ const rulesDir = path.join(ctx.cwd, ".claude", "rules");
222
+ if (!fs.existsSync(rulesDir)) return;
223
+ const files = fs.readdirSync(rulesDir, { recursive: true })
224
+ .filter(f => f.toString().endsWith(".md"));
225
+ if (files.length === 0) return;
226
+ const listing = files.map(f => `- .claude/rules/${f}`).join("\n");
227
+ event.systemPrompt += `\n\nProject rules (${files.length} files):\n${listing}\nUse the read tool to load relevant rules.`;
228
+ });
229
+ }
230
+ ```
231
+
232
+ ## Interactive Shell
233
+
234
+ Keep a persistent shell session across tool calls (rather than spawning a new shell each time):
235
+
236
+ The Pi examples include an `interactive-shell.ts` extension that maintains a persistent bash process, allowing stateful shell operations (cd, environment variables, etc.) across multiple tool invocations.
237
+
238
+ ## TUI Components
239
+
240
+ Pi's TUI library (`@mariozechner/pi-tui`) provides:
241
+
242
+ - `Text` — Word-wrapped text
243
+ - `Box` — Padded container with border
244
+ - `Container` — Vertical grouping
245
+ - `Spacer` — Vertical space
246
+ - `Markdown` — Rendered markdown
247
+ - `Image` — Terminal image display
248
+ - `SelectList` — Interactive selection list
249
+ - `BorderedLoader` — Async spinner with cancel
250
+
251
+ All components implement `render(width) → string[]` and optional `handleInput(data)`.
252
+
253
+ ## Status Line & Widgets
254
+
255
+ ```typescript
256
+ // Footer status text
257
+ ctx.ui.setStatus("🔍 Searching...");
258
+
259
+ // Widget above the editor
260
+ ctx.ui.setWidget("above", new Text("Current task: refactoring auth", 0, 0));
261
+
262
+ // Widget below the editor
263
+ ctx.ui.setWidget("below", progressComponent);
264
+
265
+ // Custom footer
266
+ ctx.ui.setFooter(new Text(theme.fg("accent", "My Extension v1.0"), 0, 0));
267
+ ```
268
+
269
+ ## File Watcher / Trigger
270
+
271
+ Run actions when files change:
272
+
273
+ ```typescript
274
+ export default function (pi: ExtensionAPI) {
275
+ pi.on("session_start", async (_event, ctx) => {
276
+ fs.watch(ctx.cwd, { recursive: true }, (eventType, filename) => {
277
+ if (filename?.endsWith(".test.ts")) {
278
+ ctx.ui.notify(`Test file changed: ${filename}`, "info");
279
+ }
280
+ });
281
+ });
282
+ }
283
+ ```
284
+
285
+ ## Input Transform
286
+
287
+ Modify user input before it reaches the LLM:
288
+
289
+ ```typescript
290
+ pi.on("input", async (event) => {
291
+ // Expand shorthand
292
+ event.text = event.text.replace(/^fix /, "Find and fix the bug: ");
293
+ // Or block certain inputs
294
+ if (event.text.includes("DELETE EVERYTHING")) {
295
+ return { block: true };
296
+ }
297
+ });
298
+ ```
299
+
300
+ ## Tool Override
301
+
302
+ Replace a built-in tool's behavior:
303
+
304
+ ```typescript
305
+ pi.on("tool_call", async (event, ctx) => {
306
+ if (event.toolName === "write") {
307
+ // Add a backup before every write
308
+ const filePath = event.input.file_path as string;
309
+ if (fs.existsSync(filePath)) {
310
+ fs.copyFileSync(filePath, `${filePath}.bak`);
311
+ }
312
+ }
313
+ });
314
+ ```
@@ -0,0 +1,212 @@
1
+ # Providers & Custom Models
2
+
3
+ Pi supports 20+ LLM providers out of the box and allows adding custom providers via `models.json` or extensions.
4
+
5
+ ## Built-in Providers
6
+
7
+ ### Subscription-based (OAuth via `/login`)
8
+
9
+ | Provider | Subscription |
10
+ |----------|-------------|
11
+ | Anthropic | Claude Pro / Max |
12
+ | OpenAI | ChatGPT Plus / Pro |
13
+ | GitHub Copilot | Copilot subscription |
14
+ | Google | Google AI / Vertex |
15
+
16
+ ### API Key Providers
17
+
18
+ | Provider | Env Variable | Auth.json Key |
19
+ |----------|-------------|---------------|
20
+ | Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
21
+ | OpenAI | `OPENAI_API_KEY` | `openai` |
22
+ | Google | `GOOGLE_API_KEY` | `google` |
23
+ | xAI | `XAI_API_KEY` | `xai` |
24
+ | Groq | `GROQ_API_KEY` | `groq` |
25
+ | Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
26
+ | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
27
+ | Mistral | `MISTRAL_API_KEY` | `mistral` |
28
+ | Hugging Face | `HF_TOKEN` | `huggingface` |
29
+ | MiniMax | `MINIMAX_API_KEY` | `minimax` |
30
+ | Together | `TOGETHER_API_KEY` | `together` |
31
+ | Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
32
+ | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
33
+ | Sambanova | `SAMBANOVA_API_KEY` | `sambanova` |
34
+
35
+ ## Credential Priority
36
+
37
+ 1. CLI flags (`--api-key`)
38
+ 2. `~/.pi/agent/auth.json` entries
39
+ 3. Environment variables
40
+ 4. Custom provider config in `models.json`
41
+
42
+ ## Auth File Format
43
+
44
+ `~/.pi/agent/auth.json` (permissions: `0600`):
45
+
46
+ ```json
47
+ {
48
+ "anthropic": {
49
+ "key": "sk-ant-..."
50
+ },
51
+ "openai": {
52
+ "key": "!op read 'OpenAI API Key'"
53
+ },
54
+ "google": {
55
+ "key": "GOOGLE_API_KEY"
56
+ }
57
+ }
58
+ ```
59
+
60
+ Key value resolution:
61
+
62
+ - **Literal:** `"sk-ant-..."` — used directly
63
+ - **Shell command:** `"!command"` — executes command, uses stdout
64
+ - **Env variable:** `"ENV_VAR_NAME"` — reads from environment
65
+
66
+ ## Custom Models via models.json
67
+
68
+ `~/.pi/agent/models.json`:
69
+
70
+ ```json
71
+ {
72
+ "providers": [
73
+ {
74
+ "name": "ollama",
75
+ "baseUrl": "http://localhost:11434/v1",
76
+ "api": "openai-completions",
77
+ "apiKey": "unused",
78
+ "models": [
79
+ {
80
+ "id": "llama3.2",
81
+ "name": "Llama 3.2",
82
+ "contextWindow": 128000,
83
+ "maxTokens": 4096,
84
+ "input": ["text"],
85
+ "reasoning": false
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ### Supported API Types
94
+
95
+ | API | Value |
96
+ |-----|-------|
97
+ | OpenAI Chat Completions | `"openai-completions"` |
98
+ | OpenAI Responses | `"openai-responses"` |
99
+ | Anthropic Messages | `"anthropic-messages"` |
100
+ | Google Generative AI | `"google-generative-ai"` |
101
+ | Google Vertex AI | `"google-vertex"` |
102
+ | Azure OpenAI | `"azure-openai-responses"` |
103
+ | Mistral | `"mistral-conversations"` |
104
+ | Amazon Bedrock | `"bedrock-converse-stream"` |
105
+
106
+ ### Model Fields
107
+
108
+ | Field | Required | Type | Description |
109
+ |-------|----------|------|-------------|
110
+ | `id` | Yes | string | Model ID sent to API |
111
+ | `name` | No | string | Human-readable display name |
112
+ | `reasoning` | No | boolean | Supports extended thinking |
113
+ | `input` | No | string[] | `["text"]` or `["text", "image"]` |
114
+ | `contextWindow` | No | number | Max context tokens |
115
+ | `maxTokens` | No | number | Max output tokens |
116
+ | `cost` | No | object | `{ input, output, cacheRead, cacheWrite }` per million tokens |
117
+
118
+ ### Provider Fields
119
+
120
+ | Field | Required | Description |
121
+ |-------|----------|-------------|
122
+ | `name` | Yes | Provider identifier |
123
+ | `baseUrl` | Yes | API endpoint URL |
124
+ | `api` | Yes | API type (see table above) |
125
+ | `apiKey` | Yes | API key (literal, env var, or shell command) |
126
+ | `models` | Yes | Array of model definitions |
127
+ | `headers` | No | Custom HTTP headers (same value resolution as apiKey) |
128
+
129
+ ### Compatibility Settings
130
+
131
+ For partially OpenAI-compatible APIs, use the `compat` field on models:
132
+
133
+ ```json
134
+ {
135
+ "compat": {
136
+ "supportsDeveloperRole": false,
137
+ "supportsReasoningEffort": false,
138
+ "supportsStreamOptions": true,
139
+ "openRouterProviderOrder": ["anthropic", "google"]
140
+ }
141
+ }
142
+ ```
143
+
144
+ ## Cloud Providers
145
+
146
+ ### Azure OpenAI
147
+
148
+ ```json
149
+ {
150
+ "name": "azure-openai",
151
+ "baseUrl": "https://myresource.openai.azure.com",
152
+ "api": "azure-openai-responses",
153
+ "apiKey": "AZURE_OPENAI_API_KEY",
154
+ "models": [{ "id": "gpt-4o", "name": "GPT-4o" }]
155
+ }
156
+ ```
157
+
158
+ ### Amazon Bedrock
159
+
160
+ Uses AWS credentials (profile, IAM keys, or bearer tokens). Set `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`. Pi auto-enables Bedrock for Claude models when credentials are available.
161
+
162
+ ### Google Vertex AI
163
+
164
+ Uses Application Default Credentials: `gcloud auth application-default login`.
165
+
166
+ ## Custom Provider via Extension
167
+
168
+ For full control (custom streaming, OAuth, etc.):
169
+
170
+ ```typescript
171
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
172
+
173
+ export default function (pi: ExtensionAPI) {
174
+ pi.registerProvider("my-provider", {
175
+ baseUrl: "https://api.example.com",
176
+ api: "openai-completions",
177
+ apiKey: "my-key",
178
+ models: [
179
+ { id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 }
180
+ ],
181
+ // Optional: custom streaming for non-standard APIs
182
+ streamSimple: async (model, context, options) => {
183
+ // Return AsyncIterable<AssistantMessageEvent>
184
+ },
185
+ // Optional: OAuth device flow
186
+ oauth: {
187
+ deviceCodeUrl: "https://auth.example.com/device",
188
+ tokenUrl: "https://auth.example.com/token",
189
+ clientId: "my-client-id",
190
+ scopes: ["chat"],
191
+ },
192
+ });
193
+ }
194
+ ```
195
+
196
+ ## Overriding Built-in Providers
197
+
198
+ To route a built-in provider through a proxy while keeping its models:
199
+
200
+ ```json
201
+ {
202
+ "providers": [
203
+ {
204
+ "name": "anthropic",
205
+ "baseUrl": "https://my-proxy.example.com/v1",
206
+ "apiKey": "proxy-key"
207
+ }
208
+ ]
209
+ }
210
+ ```
211
+
212
+ Models.json reloads automatically when accessing `/model` — no restart needed.