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 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
+ }