pi-agents-switch 0.2.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/README.md +186 -0
- package/frontmatter.ts +172 -0
- package/index.ts +760 -0
- package/package.json +26 -0
- package/profile-manager.ts +590 -0
- package/types.ts +151 -0
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# pi-agents-switch
|
|
2
|
+
|
|
3
|
+
OpenCode-style primary agent switching for [Pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
Press **F9** to cycle agents — just like Tab in OpenCode.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install ~/project/agents-switch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then `/reload` in pi.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
| Action | How |
|
|
18
|
+
|--------|-----|
|
|
19
|
+
| **Cycle agents** | Press `F9` (configurable) |
|
|
20
|
+
| **Pick agent** | `/switch` → select from list |
|
|
21
|
+
| **Configure** | `/switch-config` → create/delete/set hotkey/init builder agent |
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌──────────────────────────────────────────────────────────┐
|
|
27
|
+
│ PI (default) ←F9→ 🔨 Builder ←F9→ (your agents...) │
|
|
28
|
+
│ │ │
|
|
29
|
+
│ Uses your standard AGENTS.md in │
|
|
30
|
+
│ ~/.pi/agent/ ~/.pi/agents/ │
|
|
31
|
+
│ builder/ + your custom agents │
|
|
32
|
+
└──────────────────────────────────────────────────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 🔨 Builder Agent
|
|
36
|
+
|
|
37
|
+
The extension ships with a **Builder** agent. Initialize it with:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
/switch-config → "🎨 Initialize default agent (builder)"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The Builder understands the AGENTS.md format inside and out. Switch to it and ask:
|
|
44
|
+
|
|
45
|
+
> "Create a new agent for code review that only has read access"
|
|
46
|
+
|
|
47
|
+
The Builder will design the agent, write the AGENTS.md, and set everything up.
|
|
48
|
+
|
|
49
|
+
## Anatomy
|
|
50
|
+
|
|
51
|
+
- **`PI`** — default. No changes to your existing config. Inherits `~/.pi/agent/AGENTS.md` and everything.
|
|
52
|
+
- **Custom agents** — each is a single `AGENTS.md` with **YAML frontmatter** (metadata) + Markdown body (system prompt):
|
|
53
|
+
```
|
|
54
|
+
~/.pi/agents/<name>/
|
|
55
|
+
├── AGENTS.md ← YAML frontmatter + system prompt (all-in-one)
|
|
56
|
+
├── extensions/ ← agent-specific extensions (optional)
|
|
57
|
+
├── skills/ ← agent-specific skills (optional)
|
|
58
|
+
└── prompts/ ← agent-specific prompt templates (optional)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
When you switch to an agent, its `AGENTS.md` body is injected into the system prompt. You can also configure model, thinking level, and tools per agent via the frontmatter.
|
|
62
|
+
|
|
63
|
+
## Agent Config (AGENTS.md frontmatter)
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
---
|
|
67
|
+
name: 🎯 My Agent
|
|
68
|
+
description: Analyze and plan without making changes
|
|
69
|
+
provider: anthropic
|
|
70
|
+
model: claude-sonnet-4-5
|
|
71
|
+
thinkingLevel: medium
|
|
72
|
+
temperature: 0.7
|
|
73
|
+
topP: 0.9
|
|
74
|
+
|
|
75
|
+
# Tools: inherit from PI, then remove excluded_tools, then add tools
|
|
76
|
+
tools:
|
|
77
|
+
- read
|
|
78
|
+
- grep
|
|
79
|
+
excluded_tools:
|
|
80
|
+
- edit
|
|
81
|
+
- write
|
|
82
|
+
|
|
83
|
+
# Extensions / skills (same inheritance pattern)
|
|
84
|
+
extensions:
|
|
85
|
+
- my-custom-ext
|
|
86
|
+
noextensions:
|
|
87
|
+
- annoying-ext
|
|
88
|
+
skills:
|
|
89
|
+
- my-skill
|
|
90
|
+
noskills:
|
|
91
|
+
- noisy-skill
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
# 📋 Plan
|
|
95
|
+
|
|
96
|
+
You are the **📋 Plan** agent.
|
|
97
|
+
Your role and specific instructions go here.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Inheritance Rules
|
|
101
|
+
|
|
102
|
+
Each agent starts with PI's current config, then applies:
|
|
103
|
+
|
|
104
|
+
1. **Remove** items listed in `excluded_tools` / `noextensions` / `noskills`
|
|
105
|
+
2. **Add** items listed in `tools` / `extensions` / `skills`
|
|
106
|
+
3. If an item appears in **both** `tools` and `excluded_tools`, `tools` wins (explicit include beats exclude)
|
|
107
|
+
|
|
108
|
+
Agent-specific folders (`extensions/`, `skills/`, `prompts/`) are always included in addition to inherited items.
|
|
109
|
+
|
|
110
|
+
### Fallback Chain
|
|
111
|
+
|
|
112
|
+
When resolving an agent's config:
|
|
113
|
+
|
|
114
|
+
1. **Agent-level**: `~/.pi/agents/<name>/AGENTS.md`
|
|
115
|
+
2. **Project-level**: `<cwd>/.pi/agents/<name>/AGENTS.md` — overrides agent-level fields
|
|
116
|
+
3. **PI defaults**: tools/extensions/skills from your running PI config
|
|
117
|
+
|
|
118
|
+
## Creating agents
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
/switch-config → "➕ Create a new agent"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or create files directly:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
mkdir -p ~/.pi/agents/debug
|
|
128
|
+
|
|
129
|
+
cat > ~/.pi/agents/debug/AGENTS.md << 'EOF'
|
|
130
|
+
---
|
|
131
|
+
name: 🐛 Debug
|
|
132
|
+
description: Debugging specialist
|
|
133
|
+
tools:
|
|
134
|
+
- read
|
|
135
|
+
- bash
|
|
136
|
+
- grep
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
# 🐛 Debug
|
|
140
|
+
|
|
141
|
+
You are a debugging specialist. Focus on:
|
|
142
|
+
- Root cause analysis
|
|
143
|
+
- Reproducing bugs
|
|
144
|
+
- Minimal fixes
|
|
145
|
+
EOF
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Agents are auto-discovered from the filesystem — no registration needed.
|
|
149
|
+
|
|
150
|
+
## Config
|
|
151
|
+
|
|
152
|
+
`~/.pi/agent/agents-switch.json` (minimal — only tracks hotkey + active agent):
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"version": 1,
|
|
157
|
+
"hotkey": "f9",
|
|
158
|
+
"active": "PI"
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Frontmatter Reference
|
|
163
|
+
|
|
164
|
+
| Field | Type | Description |
|
|
165
|
+
|-------|------|-------------|
|
|
166
|
+
| `name` | string | Display name (e.g. `"🔨 Builder"`) |
|
|
167
|
+
| `description` | string | Short description shown in picker |
|
|
168
|
+
| `provider` | string | Model provider (e.g. `"anthropic"`) |
|
|
169
|
+
| `model` | string | Model ID (e.g. `"claude-sonnet-4-5"`) |
|
|
170
|
+
| `thinkingLevel` | string | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
|
|
171
|
+
| `temperature` | number | Sampling temperature (0-2). Lower = more deterministic |
|
|
172
|
+
| `topP` | number | Nucleus sampling threshold (0-1) |
|
|
173
|
+
| `tools` | string[] | Tools to ADD after inheritance |
|
|
174
|
+
| `excluded_tools` | string[] | Tools to REMOVE from PI's inherited set |
|
|
175
|
+
| `extensions` | string[] | Extensions to ADD |
|
|
176
|
+
| `noextensions` | string[] | Extensions to REMOVE |
|
|
177
|
+
| `skills` | string[] | Skills to ADD |
|
|
178
|
+
| `noskills` | string[] | Skills to REMOVE |
|
|
179
|
+
|
|
180
|
+
The Markdown body (everything after the last `---`) is the **system prompt** injected before each agent turn.
|
|
181
|
+
|
|
182
|
+
## Inspired by
|
|
183
|
+
|
|
184
|
+
- [OpenCode agents](https://opencode.ai/docs/agents) — Tab cycling + primary agents
|
|
185
|
+
- [pi-profiles](https://github.com/chaychoong/pi-profiles) — Profile isolation pattern
|
|
186
|
+
- [pi-cycle](https://github.com/jerryfan/pi-cycle) — Hotkey + model switching pattern
|
package/frontmatter.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML frontmatter parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses the `---` delimited block at the start of a markdown file.
|
|
5
|
+
* Handles the subset of YAML we need: string values, arrays of strings,
|
|
6
|
+
* and the special `true`/`false` booleans.
|
|
7
|
+
*
|
|
8
|
+
* Also tracks the order of top-level keys for last-write-wins
|
|
9
|
+
* conflict resolution between include/exclude lists.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AgentFrontmatter, FrontmatterResult } from "./types";
|
|
13
|
+
|
|
14
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a markdown string with optional YAML frontmatter.
|
|
18
|
+
* If no frontmatter block is found, returns an empty frontmatter
|
|
19
|
+
* and the entire content as the body.
|
|
20
|
+
*/
|
|
21
|
+
export function parseFrontmatter(content: string): FrontmatterResult {
|
|
22
|
+
const match = content.match(FRONTMATTER_RE);
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
return {
|
|
26
|
+
frontmatter: {},
|
|
27
|
+
body: content.trimStart(),
|
|
28
|
+
keyOrder: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const yamlBlock = match[1];
|
|
33
|
+
const body = content.slice(match[0].length).trimStart();
|
|
34
|
+
|
|
35
|
+
// Parse the YAML subset
|
|
36
|
+
const fm: AgentFrontmatter = {};
|
|
37
|
+
const keyOrder: string[] = [];
|
|
38
|
+
const lines = yamlBlock.split("\n");
|
|
39
|
+
|
|
40
|
+
let currentKey: string | null = null;
|
|
41
|
+
let currentArray: string[] = [];
|
|
42
|
+
|
|
43
|
+
function flushArray(): void {
|
|
44
|
+
if (currentKey && currentArray.length > 0) {
|
|
45
|
+
(fm as any)[currentKey] = currentArray;
|
|
46
|
+
}
|
|
47
|
+
currentKey = null;
|
|
48
|
+
currentArray = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const rawLine of lines) {
|
|
52
|
+
const line = rawLine.trimEnd();
|
|
53
|
+
|
|
54
|
+
// Empty lines: flush current array
|
|
55
|
+
if (line.trim() === "") {
|
|
56
|
+
flushArray();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Array item: ` - value` or `- value`
|
|
61
|
+
const arrayMatch = line.match(/^\s*-\s+(.+)$/);
|
|
62
|
+
if (arrayMatch) {
|
|
63
|
+
currentArray.push(arrayMatch[1].trim());
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Key-value: `key: value`
|
|
68
|
+
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
|
|
69
|
+
if (kvMatch) {
|
|
70
|
+
flushArray();
|
|
71
|
+
currentKey = kvMatch[1];
|
|
72
|
+
keyOrder.push(currentKey); // Track key order for last-write-wins
|
|
73
|
+
const value = kvMatch[2].trim();
|
|
74
|
+
|
|
75
|
+
if (value === "") {
|
|
76
|
+
// Key with no value — might be the start of an array
|
|
77
|
+
currentArray = [];
|
|
78
|
+
} else {
|
|
79
|
+
// Scalar value
|
|
80
|
+
(fm as any)[currentKey] = parseScalar(value);
|
|
81
|
+
currentKey = null;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Comment: `# ...` — ignore
|
|
87
|
+
if (line.trim().startsWith("#")) {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Flush any remaining array
|
|
92
|
+
flushArray();
|
|
93
|
+
|
|
94
|
+
return { frontmatter: fm, body, keyOrder };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse a scalar YAML value.
|
|
99
|
+
* Handles: unquoted strings, quoted strings, booleans, numbers.
|
|
100
|
+
*/
|
|
101
|
+
function parseScalar(value: string): string | boolean | number {
|
|
102
|
+
// Quoted strings
|
|
103
|
+
if (
|
|
104
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
105
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
106
|
+
) {
|
|
107
|
+
return value.slice(1, -1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Booleans
|
|
111
|
+
const lower = value.toLowerCase();
|
|
112
|
+
if (lower === "true") return true;
|
|
113
|
+
if (lower === "false") return false;
|
|
114
|
+
|
|
115
|
+
// Numbers
|
|
116
|
+
if (/^-?\d+(\.\d+)?$/.test(value) && !value.startsWith("0")) {
|
|
117
|
+
return Number(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Empty / null
|
|
121
|
+
if (lower === "null" || lower === "~" || value === "") {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Serialize frontmatter back to a YAML block (for creating agents).
|
|
130
|
+
* Uses the preferred key names: `excluded_tools`, `excluded_extensions`, `excluded_skills`.
|
|
131
|
+
*/
|
|
132
|
+
export function serializeFrontmatter(fm: AgentFrontmatter): string {
|
|
133
|
+
const lines: string[] = ["---"];
|
|
134
|
+
|
|
135
|
+
if (fm.name) lines.push(`name: ${quoteIfNeeded(fm.name)}`);
|
|
136
|
+
if (fm.description)
|
|
137
|
+
lines.push(`description: ${quoteIfNeeded(fm.description)}`);
|
|
138
|
+
if (fm.provider) lines.push(`provider: ${fm.provider}`);
|
|
139
|
+
if (fm.model) lines.push(`model: ${fm.model}`);
|
|
140
|
+
if (fm.thinkingLevel) lines.push(`thinkingLevel: ${fm.thinkingLevel}`);
|
|
141
|
+
if (fm.temperature !== undefined)
|
|
142
|
+
lines.push(`temperature: ${fm.temperature}`);
|
|
143
|
+
if (fm.topP !== undefined) lines.push(`topP: ${fm.topP}`);
|
|
144
|
+
|
|
145
|
+
function writeArray(key: string, items: string[] | undefined): void {
|
|
146
|
+
if (!items || items.length === 0) return;
|
|
147
|
+
lines.push(`${key}:`);
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
lines.push(` - ${item}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
writeArray("tools", fm.tools);
|
|
154
|
+
writeArray("excluded_tools", fm.excluded_tools);
|
|
155
|
+
writeArray("extensions", fm.extensions);
|
|
156
|
+
writeArray("excluded_extensions", fm.excluded_extensions ?? fm.noextensions);
|
|
157
|
+
writeArray("skills", fm.skills);
|
|
158
|
+
writeArray("excluded_skills", fm.excluded_skills ?? fm.noskills);
|
|
159
|
+
|
|
160
|
+
lines.push("---");
|
|
161
|
+
lines.push(""); // blank line before body
|
|
162
|
+
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function quoteIfNeeded(value: string): string {
|
|
167
|
+
// If the value contains `:` or `#` or starts with special chars, quote it
|
|
168
|
+
if (/[:#]/.test(value) || /^["'{[-]/.test(value)) {
|
|
169
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|