@matyah00/openpi 0.1.3 → 0.1.5
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/LICENSE +21 -0
- package/README.md +335 -73
- package/extensions/agent-chain.ts +45 -47
- package/extensions/agent-team.ts +32 -28
- package/extensions/commands.ts +8 -43
- package/extensions/damage-control-continue.ts +2 -2
- package/extensions/lib/markdown.ts +27 -0
- package/extensions/minimal.ts +3 -3
- package/extensions/openpi.ts +1 -1
- package/extensions/pure-focus.ts +1 -1
- package/extensions/purpose-gate.ts +2 -2
- package/extensions/system-select.ts +6 -16
- package/extensions/theme-cycler.ts +2 -2
- package/extensions/themeMap.ts +1 -1
- package/extensions/tool-counter-widget.ts +2 -2
- package/extensions/tool-counter.ts +3 -3
- package/package.json +9 -1
- package/scripts/validate-package.mjs +100 -0
- package/types/pi-shims.d.ts +0 -48
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 haytamAroui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>OpenPi</h1>
|
|
3
|
+
<p><strong>A Pi-native command, skill, agent, workflow, and theme pack for serious coding sessions.</strong></p>
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@matyah00/openpi"><img src="https://img.shields.io/npm/v/@matyah00/openpi.svg" alt="npm version"></a>
|
|
6
|
+
<a href="https://github.com/haytamAroui/OpenPi/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
|
|
7
|
+
<a href="https://github.com/haytamAroui/OpenPi"><img src="https://img.shields.io/badge/Pi-native-111827" alt="Pi native"></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@matyah00/openpi"><img src="https://img.shields.io/badge/install-pi%20install%20npm%3A%40matyah00%2Fopenpi-0f766e" alt="Pi install"></a>
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
<a href="#the-core-idea">Core Idea</a>
|
|
12
|
+
<a href="#install">Install</a>
|
|
13
|
+
<a href="#quick-start">Quick Start</a>
|
|
14
|
+
<a href="#what-you-get">What You Get</a>
|
|
15
|
+
<a href="#profiles">Profiles</a>
|
|
16
|
+
<a href="#commands">Commands</a>
|
|
17
|
+
<a href="#agent-workflows">Agents</a>
|
|
18
|
+
<a href="#tooling">Tools</a>
|
|
19
|
+
<a href="#architecture">Architecture</a>
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
2
22
|
|
|
3
|
-
|
|
23
|
+
---
|
|
4
24
|
|
|
5
|
-
|
|
25
|
+
## The Core Idea
|
|
26
|
+
|
|
27
|
+
**OpenPi turns Pi into a richer engineering environment without copying another assistant's file layout.**
|
|
28
|
+
|
|
29
|
+
It uses Pi-owned package surfaces:
|
|
6
30
|
|
|
7
31
|
- `prompts/*.md` for slash-command prompt templates
|
|
8
32
|
- `skills/*/SKILL.md` for Pi skills
|
|
9
|
-
- `agents/*.md` for
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
33
|
+
- `agents/*.md` for specialist role prompts
|
|
34
|
+
- `extensions/*.ts` for native Pi commands, tools, profile switching, themes, and workflow orchestration
|
|
35
|
+
- `themes/*.json` for terminal UI themes
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Plain Pi session OpenPi session
|
|
39
|
+
|
|
40
|
+
Manual project scan /prime and /explore orient the session
|
|
41
|
+
Ad hoc planning /spec, /clarify, /blueprint, /debate
|
|
42
|
+
One long assistant context spawn_agents delegates to role agents
|
|
43
|
+
Manual grep/tree work project_tree and code_search_batch
|
|
44
|
+
Manual safety checks secret_scan, env_scan, ghost_test_scan
|
|
45
|
+
Lost session reasoning /snapshot, goal_state, write_snapshot
|
|
46
|
+
One profile for every task /openpi use <profile>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
OpenPi is not a project template. It is a reusable Pi package you install once and activate per project or globally.
|
|
50
|
+
|
|
51
|
+
---
|
|
12
52
|
|
|
13
53
|
## Install
|
|
14
54
|
|
|
@@ -22,96 +62,318 @@ For Pi:
|
|
|
22
62
|
pi install npm:@matyah00/openpi
|
|
23
63
|
```
|
|
24
64
|
|
|
25
|
-
Restart Pi after install
|
|
65
|
+
Restart Pi after install, then activate a profile:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
/openpi use full
|
|
69
|
+
/reload
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The npm names `openpi` and `open-pi` are blocked by npm's package-name similarity policy, so the public npm package is:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
@matyah00/openpi
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
/openpi list show every profile
|
|
84
|
+
/openpi use commands enable prompt commands and core tools
|
|
85
|
+
/openpi use workflow enable /add, /fix, /review and spawn_agents
|
|
86
|
+
/openpi use guard enable security, dependency, and ship gates
|
|
87
|
+
/openpi use full enable the broad OpenPi surface
|
|
88
|
+
/openpi clear remove OpenPi-managed extensions
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use `--global` to install the selected profile into your global Pi settings:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
/openpi use full --global
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Profiles update `.pi/settings.json`. Run `/reload` or restart Pi after switching.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## What You Get
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
OpenPi
|
|
105
|
+
13 profiles choose a focused runtime surface per task
|
|
106
|
+
19 prompt commands /prime, /blueprint, /deep, /ship, /parallel, ...
|
|
107
|
+
36 agent prompts planner, reviewer, tester, security-auditor, Pi experts
|
|
108
|
+
7 skills ultrathink, test-first, security-guard, bowser, ...
|
|
109
|
+
11 themes tokyo-night, rose-pine, gruvbox, nord, dracula, ...
|
|
110
|
+
Native tools search, audit, state, snapshot, dispatch, chains
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
OpenPi is designed around one practical rule: load only the surface you need.
|
|
114
|
+
|
|
115
|
+
| Need | Profile | What it adds |
|
|
116
|
+
|------|---------|--------------|
|
|
117
|
+
| Slash commands | `commands` | Prompt commands, search tools, state tools, theme switching |
|
|
118
|
+
| Explore a repo | `explore` | Tree/search/discovery workflows |
|
|
119
|
+
| Implement work | `workflow` | `/add`, `/fix`, `/review`, `spawn_agents` |
|
|
120
|
+
| Pre-ship safety | `guard` | Security, dependency, environment, and test-integrity tools |
|
|
121
|
+
| Focused UI | `focus` | Minimal Pi UI plus theme defaults |
|
|
122
|
+
| Session purpose | `purpose` | Keeps task purpose visible while working |
|
|
123
|
+
| Metrics | `metrics` | Model, context, branch, token, cost, and tool counters |
|
|
124
|
+
| Personas | `system` | Switch active system persona from bundled agents |
|
|
125
|
+
| Teams | `team` | Dispatcher-only role team with `dispatch_agent` |
|
|
126
|
+
| Chains | `chain` | Sequential workflow runner with `run_chain` |
|
|
127
|
+
| Everything | `full` | Commands, workflow, safety, metrics, personas, teams, chains |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
OpenPi prompt commands are markdown templates with frontmatter metadata. They are loaded from the package and can be extended by project-local `.pi/prompts/*.md`.
|
|
134
|
+
|
|
135
|
+
### Planning and reasoning
|
|
136
|
+
|
|
137
|
+
| Command | Purpose |
|
|
138
|
+
|---------|---------|
|
|
139
|
+
| `/prime` | Load project orientation before work |
|
|
140
|
+
| `/spec` | Turn a vague feature into a structured spec |
|
|
141
|
+
| `/clarify` | Ask focused clarification questions before coding |
|
|
142
|
+
| `/blueprint` | Build an implementation blueprint before risky work |
|
|
143
|
+
| `/debate` | Run adversarial decision analysis for tradeoffs |
|
|
144
|
+
| `/deep` | Use deeper evidence-led reasoning for hard bugs or architecture |
|
|
145
|
+
| `/plan-team` | Create a team-oriented implementation plan |
|
|
146
|
+
|
|
147
|
+
### Discovery and state
|
|
148
|
+
|
|
149
|
+
| Command | Purpose |
|
|
150
|
+
|---------|---------|
|
|
151
|
+
| `/explore` | Map relevant files and code paths |
|
|
152
|
+
| `/goal` | Manage durable task goals in `.pi/memory/goals.md` |
|
|
153
|
+
| `/snapshot` | Save a continuation checkpoint before compaction |
|
|
154
|
+
| `/compress` | Compress current task context into a handoff brief |
|
|
155
|
+
| `/commands` | List loaded OpenPi and project prompt commands |
|
|
156
|
+
| `/command:status` | Show command loader status and collisions |
|
|
157
|
+
|
|
158
|
+
### Quality and release
|
|
159
|
+
|
|
160
|
+
| Command | Purpose |
|
|
161
|
+
|---------|---------|
|
|
162
|
+
| `/code-review` | Review code or changes for bugs and missing tests |
|
|
163
|
+
| `/test` | Select or design focused validation |
|
|
164
|
+
| `/validate` | Run targeted validation for current work |
|
|
165
|
+
| `/deps` | Audit dependency manifests, lockfiles, pins, and update risk |
|
|
166
|
+
| `/ghost-test` | Detect vacuous tests and reward-hacking test patterns |
|
|
167
|
+
| `/sentinel` | Read-only security and environment safety scan |
|
|
168
|
+
| `/ship` | Pre-ship gate for tests, security, dependency risk, and git readiness |
|
|
169
|
+
| `/parallel` | Plan safe parallel work with ownership checks |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Agent Workflows
|
|
174
|
+
|
|
175
|
+
The `workflow` profile adds high-level commands that prompt Pi to use role agents and verification steps.
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
/add <feature> discover files -> plan -> edit -> test -> review
|
|
179
|
+
/fix <bug> reproduce/inspect -> plan -> patch -> validate -> review
|
|
180
|
+
/review [scope] read-only review over diff or requested scope
|
|
181
|
+
/openpi-agents list available role agents
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
OpenPi role agents include:
|
|
185
|
+
|
|
186
|
+
| Agent | Role |
|
|
187
|
+
|-------|------|
|
|
188
|
+
| `file-picker` | Finds relevant files and line ranges before edits |
|
|
189
|
+
| `planner` | Produces scoped implementation plans |
|
|
190
|
+
| `editor` | Performs isolated edits when the plan is clear |
|
|
191
|
+
| `tester` | Selects and runs targeted validation |
|
|
192
|
+
| `reviewer` | Reviews diffs for bugs, regressions, and missing tests |
|
|
193
|
+
| `security-auditor` | Checks secrets, risky automation, and security-sensitive changes |
|
|
194
|
+
| `problem-architect` | Turns ambiguous work into a concrete team spec |
|
|
195
|
+
| `spec-reviewer` | Challenges unclear requirements before implementation |
|
|
196
|
+
| `ship-guard` | Reviews release readiness |
|
|
197
|
+
| `red-team` | Challenges plans and assumptions |
|
|
198
|
+
|
|
199
|
+
The `spawn_agents` tool can run agents sequentially or in parallel as isolated Pi subprocesses. It returns structured outputs: files, line ranges, commands, exact validation output, findings, and assumptions.
|
|
200
|
+
|
|
201
|
+
---
|
|
26
202
|
|
|
27
|
-
|
|
28
|
-
similarity policy, so this package uses the public scoped name
|
|
29
|
-
`@matyah00/openpi`.
|
|
203
|
+
## Team and Chain Modes
|
|
30
204
|
|
|
31
|
-
|
|
205
|
+
OpenPi has two orchestration modes for larger work.
|
|
32
206
|
|
|
33
|
-
|
|
207
|
+
### Team dispatcher
|
|
34
208
|
|
|
35
209
|
```text
|
|
36
|
-
/openpi use commands
|
|
37
|
-
/openpi use explore
|
|
38
|
-
/openpi use guard
|
|
39
|
-
/openpi use workflow
|
|
40
|
-
/openpi use system
|
|
41
210
|
/openpi use team
|
|
211
|
+
/agents-list
|
|
212
|
+
/agents-team guard
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `team` profile registers `dispatch_agent` and uses teams from `agents/teams.yaml`, including:
|
|
216
|
+
|
|
217
|
+
| Team | Agents |
|
|
218
|
+
|------|--------|
|
|
219
|
+
| `research` | scout, directory-lister, glob-matcher, code-searcher, librarian, documenter, red-team |
|
|
220
|
+
| `validation` | tester, basher, reviewer |
|
|
221
|
+
| `guard` | security-auditor, rule-verifier, ship-guard, spec-reviewer |
|
|
222
|
+
| `frontend` | scout, frontend, reviewer |
|
|
223
|
+
| `backend` | scout, backend, reviewer |
|
|
224
|
+
| `pi-pi` | Pi package, extension, skill, prompt, config, theme, TUI, CLI, and keybinding experts |
|
|
225
|
+
|
|
226
|
+
### Chain runner
|
|
227
|
+
|
|
228
|
+
```text
|
|
42
229
|
/openpi use chain
|
|
43
|
-
/
|
|
44
|
-
/openpi clear
|
|
230
|
+
/chain-list
|
|
45
231
|
```
|
|
46
232
|
|
|
47
|
-
|
|
48
|
-
Run `/reload` or restart Pi after changing profiles.
|
|
233
|
+
The `chain` profile registers `run_chain` for sequential workflows from `agents/agent-chain.yaml`:
|
|
49
234
|
|
|
50
|
-
|
|
235
|
+
| Chain | Flow |
|
|
236
|
+
|-------|------|
|
|
237
|
+
| `plan-build-review` | planner -> builder -> reviewer |
|
|
238
|
+
| `research-plan` | scout -> red-team -> planner |
|
|
239
|
+
| `deep-explore` | directory-lister -> code-searcher -> thinker |
|
|
240
|
+
| `evidence-validate` | code-searcher -> tester -> reviewer |
|
|
241
|
+
| `spec-to-plan` | problem-architect -> spec-reviewer -> planner |
|
|
242
|
+
| `ship-gate` | security-auditor -> ship-guard -> reviewer |
|
|
243
|
+
| `pi-package-design` | Pi experts -> planner |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Tooling
|
|
248
|
+
|
|
249
|
+
OpenPi registers native Pi tools through profiles.
|
|
250
|
+
|
|
251
|
+
| Tool | Purpose |
|
|
252
|
+
|------|---------|
|
|
253
|
+
| `project_tree` | Return a scoped project tree with ignore handling |
|
|
254
|
+
| `code_search_batch` | Run multiple code searches in one call |
|
|
255
|
+
| `env_scan` | Detect stack, package managers, scripts, and environment clues |
|
|
256
|
+
| `secret_scan` | Search for common secret and credential patterns |
|
|
257
|
+
| `ghost_test_scan` | Find weak, vacuous, or reward-hacked tests |
|
|
258
|
+
| `dependency_inventory` | Summarize dependency manifests and lockfiles |
|
|
259
|
+
| `session_state` | Read current session state |
|
|
260
|
+
| `goal_state` | Read goal memory state |
|
|
261
|
+
| `write_snapshot` | Write a continuation snapshot |
|
|
262
|
+
| `parallel_safety_check` | Check file ownership overlap before parallel work |
|
|
263
|
+
| `spawn_agents` | Run role agents as isolated Pi subprocesses |
|
|
264
|
+
| `dispatch_agent` | Dispatch to the active specialist team |
|
|
265
|
+
| `run_chain` | Run a named sequential agent chain |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Skills
|
|
270
|
+
|
|
271
|
+
OpenPi ships focused Pi skills:
|
|
272
|
+
|
|
273
|
+
| Skill | Use it for |
|
|
274
|
+
|-------|------------|
|
|
275
|
+
| `ultrathink` | Hard debugging, architecture tradeoffs, root-cause analysis |
|
|
276
|
+
| `test-first` | Production code, bug fixes, refactors, validation planning |
|
|
277
|
+
| `security-guard` | Credentials, env files, external code, automation, deploy risk |
|
|
278
|
+
| `spec-driven` | Vague requirements, unclear features, acceptance criteria |
|
|
279
|
+
| `session-continuity` | Long context, resuming, stopping, compaction handoffs |
|
|
280
|
+
| `env-scanner` | Unknown repos, setup issues, stack detection |
|
|
281
|
+
| `bowser` | Playwright-powered browser automation and UI testing |
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Themes and UI Profiles
|
|
51
286
|
|
|
52
|
-
|
|
287
|
+
OpenPi includes 11 bundled themes:
|
|
53
288
|
|
|
54
289
|
```text
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/
|
|
64
|
-
/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
290
|
+
catppuccin-mocha cyberpunk dracula everforest gruvbox
|
|
291
|
+
midnight-ocean nord ocean-breeze rose-pine
|
|
292
|
+
synthwave tokyo-night
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Use the theme profile or the `full` profile, then:
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
/theme
|
|
299
|
+
/theme tokyo-night
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
UI-oriented profiles:
|
|
303
|
+
|
|
304
|
+
| Profile | Purpose |
|
|
305
|
+
|---------|---------|
|
|
306
|
+
| `focus` | Minimal, distraction-free Pi UI |
|
|
307
|
+
| `metrics` | Status bar and usage metrics |
|
|
308
|
+
| `tool-widget` | Live per-tool usage counts |
|
|
309
|
+
| `purpose` | Session purpose gate and visible task focus |
|
|
310
|
+
| `safety` | Damage-control rules and actionable blocked-tool feedback |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Architecture
|
|
315
|
+
|
|
316
|
+
OpenPi is a package, not a project scaffold.
|
|
77
317
|
|
|
78
318
|
```text
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
write_snapshot
|
|
88
|
-
parallel_safety_check
|
|
319
|
+
@matyah00/openpi
|
|
320
|
+
package.json Pi package manifest
|
|
321
|
+
prompts/ prompt commands
|
|
322
|
+
skills/ Pi skills
|
|
323
|
+
agents/ role agents, teams, chains
|
|
324
|
+
extensions/ native Pi extensions and tools
|
|
325
|
+
themes/ terminal UI themes
|
|
326
|
+
damage-control-rules.yaml safety feedback rules
|
|
89
327
|
```
|
|
90
328
|
|
|
91
|
-
|
|
329
|
+
The activation flow is explicit:
|
|
92
330
|
|
|
93
331
|
```text
|
|
94
|
-
|
|
95
|
-
/
|
|
96
|
-
/
|
|
97
|
-
/
|
|
332
|
+
1. Install package with Pi
|
|
333
|
+
2. Run /openpi use <profile>
|
|
334
|
+
3. OpenPi writes selected extension paths into .pi/settings.json
|
|
335
|
+
4. Run /reload or restart Pi
|
|
336
|
+
5. Use only the commands/tools for that profile
|
|
98
337
|
```
|
|
99
338
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
339
|
+
This keeps OpenPi portable across projects. You can add project-specific prompts and agents in `.pi/prompts` and `.pi/agents`; OpenPi will load them alongside the bundled resources where the active profile supports it.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Why It Is Different
|
|
105
344
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
345
|
+
| Without OpenPi | With OpenPi |
|
|
346
|
+
|----------------|-------------|
|
|
347
|
+
| One generic assistant flow | Profiles for planning, workflow, guard, team, chain, UI |
|
|
348
|
+
| Manual project discovery | `/explore`, `project_tree`, `code_search_batch` |
|
|
349
|
+
| Vague implementation prompts | `/spec`, `/clarify`, `/blueprint`, `problem-architect` |
|
|
350
|
+
| Manual review discipline | `/review`, `reviewer`, `ship-guard`, `ghost_test_scan` |
|
|
351
|
+
| One large context | `spawn_agents`, `dispatch_agent`, `run_chain` |
|
|
352
|
+
| Repeated session loss | `/snapshot`, `write_snapshot`, `goal_state` |
|
|
353
|
+
| Ad hoc security checks | `secret_scan`, `/sentinel`, `security-guard` |
|
|
354
|
+
| Same UI for every task | focus, metrics, purpose, tool-widget, safety profiles |
|
|
109
355
|
|
|
110
|
-
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Verified
|
|
359
|
+
|
|
360
|
+
Before publish, the package is checked with:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npm pack --dry-run
|
|
364
|
+
npx tsc --noEmit
|
|
365
|
+
npm install @matyah00/openpi@latest --dry-run
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Current npm package:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
npm i @matyah00/openpi
|
|
372
|
+
pi install npm:@matyah00/openpi
|
|
373
|
+
```
|
|
111
374
|
|
|
112
|
-
|
|
375
|
+
---
|
|
113
376
|
|
|
114
|
-
|
|
115
|
-
`dispatch_agent` to delegate work to the selected team.
|
|
377
|
+
## License
|
|
116
378
|
|
|
117
|
-
|
|
379
|
+
MIT - [haytamAroui](https://github.com/haytamAroui)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
6
7
|
import { join, resolve } from "path";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
7
9
|
import { bundledAgentsDir } from "./lib/packagePaths.ts";
|
|
10
|
+
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
8
11
|
|
|
9
12
|
type ChainStep = { agent: string; prompt: string };
|
|
10
13
|
type ChainDef = { name: string; description: string; steps: ChainStep[] };
|
|
@@ -16,57 +19,48 @@ function displayName(name: string): string {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
function parseChainYaml(raw: string): ChainDef[] {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
let currentStep: ChainStep | null = null;
|
|
22
|
+
const parsed = parseYaml(raw);
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
currentStep = { agent: agentMatch[1].trim(), prompt: "" };
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const promptMatch = line.match(/^\s+prompt:\s+(.+)$/);
|
|
44
|
-
if (promptMatch && currentStep) {
|
|
45
|
-
currentStep.prompt = promptMatch[1].trim().replace(/^["']|["']$/g, "").replace(/\\n/g, "\n");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (current && currentStep) current.steps.push(currentStep);
|
|
49
|
-
return chains;
|
|
25
|
+
return Object.entries(parsed as Record<string, unknown>).flatMap(([name, value]) => {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
27
|
+
const chain = value as { description?: unknown; steps?: unknown };
|
|
28
|
+
if (!Array.isArray(chain.steps)) return [];
|
|
29
|
+
const steps = chain.steps.flatMap((step): ChainStep[] => {
|
|
30
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) return [];
|
|
31
|
+
const item = step as { agent?: unknown; prompt?: unknown };
|
|
32
|
+
if (typeof item.agent !== "string" || typeof item.prompt !== "string") return [];
|
|
33
|
+
return [{ agent: item.agent, prompt: item.prompt }];
|
|
34
|
+
});
|
|
35
|
+
if (!steps.length) return [];
|
|
36
|
+
return [{
|
|
37
|
+
name,
|
|
38
|
+
description: typeof chain.description === "string" ? chain.description : "",
|
|
39
|
+
steps,
|
|
40
|
+
}];
|
|
41
|
+
});
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
function parseAgentFile(filePath: string): AgentDef | null {
|
|
53
45
|
const raw = readFileSync(filePath, "utf-8");
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const line of match[1].split("\n")) {
|
|
58
|
-
const idx = line.indexOf(":");
|
|
59
|
-
if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
60
|
-
}
|
|
61
|
-
if (!frontmatter.name) return null;
|
|
46
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(raw);
|
|
47
|
+
const name = stringField(frontmatter.name);
|
|
48
|
+
if (!name) return null;
|
|
62
49
|
return {
|
|
63
|
-
name
|
|
64
|
-
description: frontmatter.description
|
|
65
|
-
tools: frontmatter.tools || "read,grep,find,ls",
|
|
66
|
-
systemPrompt:
|
|
50
|
+
name,
|
|
51
|
+
description: stringField(frontmatter.description),
|
|
52
|
+
tools: arrayField(frontmatter.tools).join(",") || "read,grep,find,ls",
|
|
53
|
+
systemPrompt: body,
|
|
67
54
|
};
|
|
68
55
|
}
|
|
69
56
|
|
|
57
|
+
function writeSystemPromptFile(agentName: string, systemPrompt: string): { dir: string; filePath: string } {
|
|
58
|
+
const dir = mkdtempSync(join(tmpdir(), "openpi-chain-"));
|
|
59
|
+
const filePath = join(dir, `system-${agentName.replace(/[^\w.-]+/g, "_")}.md`);
|
|
60
|
+
writeFileSync(filePath, systemPrompt, { encoding: "utf-8", mode: 0o600 });
|
|
61
|
+
return { dir, filePath };
|
|
62
|
+
}
|
|
63
|
+
|
|
70
64
|
function scanAgentDirs(cwd: string): Map<string, AgentDef> {
|
|
71
65
|
const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
|
|
72
66
|
const agents = new Map<string, AgentDef>();
|
|
@@ -125,13 +119,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
125
119
|
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
|
|
126
120
|
const agentKey = agentDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
127
121
|
const agentSessionFile = join(sessionDir, `chain-${agentKey}.json`);
|
|
122
|
+
const systemPrompt = writeSystemPromptFile(agentKey, agentDef.systemPrompt);
|
|
128
123
|
const args = [
|
|
129
124
|
"--mode", "json",
|
|
130
125
|
"-p",
|
|
131
126
|
"--no-extensions",
|
|
132
127
|
"--tools", agentDef.tools,
|
|
133
128
|
"--thinking", "off",
|
|
134
|
-
"--append-system-prompt",
|
|
129
|
+
"--append-system-prompt", systemPrompt.filePath,
|
|
135
130
|
"--session", agentSessionFile,
|
|
136
131
|
];
|
|
137
132
|
if (model) args.splice(4, 0, "--model", model);
|
|
@@ -143,6 +138,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
138
|
const chunks: string[] = [];
|
|
144
139
|
|
|
145
140
|
return new Promise((resolveDone) => {
|
|
141
|
+
const cleanupPrompt = () => rmSync(systemPrompt.dir, { recursive: true, force: true });
|
|
146
142
|
const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
|
|
147
143
|
const timer = setInterval(() => {
|
|
148
144
|
state.elapsed = Date.now() - start;
|
|
@@ -171,12 +167,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
167
|
proc.stderr!.on("data", () => {});
|
|
172
168
|
proc.on("close", (code) => {
|
|
173
169
|
clearInterval(timer);
|
|
170
|
+
cleanupPrompt();
|
|
174
171
|
state.elapsed = Date.now() - start;
|
|
175
172
|
if (code === 0) agentSessions.set(agentKey, agentSessionFile);
|
|
176
173
|
resolveDone({ output: chunks.join(""), exitCode: code ?? 1, elapsed: state.elapsed });
|
|
177
174
|
});
|
|
178
175
|
proc.on("error", (error) => {
|
|
179
176
|
clearInterval(timer);
|
|
177
|
+
cleanupPrompt();
|
|
180
178
|
resolveDone({ output: `Error spawning agent: ${error.message}`, exitCode: 1, elapsed: Date.now() - start });
|
|
181
179
|
});
|
|
182
180
|
});
|
package/extensions/agent-team.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
2
|
-
import { Type } from "
|
|
3
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
6
7
|
import { join, resolve } from "path";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
7
9
|
import { bundledAgentsDir } from "./lib/packagePaths.ts";
|
|
10
|
+
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
8
11
|
|
|
9
12
|
type AgentDef = {
|
|
10
13
|
name: string;
|
|
@@ -28,42 +31,39 @@ function displayName(name: string): string {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function parseTeamsYaml(raw: string): Record<string, string[]> {
|
|
34
|
+
const parsed = parseYaml(raw);
|
|
35
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
36
|
+
|
|
31
37
|
const teams: Record<string, string[]> = {};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (teamMatch) {
|
|
36
|
-
current = teamMatch[1].trim();
|
|
37
|
-
teams[current] = [];
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const itemMatch = line.match(/^\s+-\s+(.+)$/);
|
|
41
|
-
if (itemMatch && current) teams[current].push(itemMatch[1].trim());
|
|
38
|
+
for (const [name, members] of Object.entries(parsed as Record<string, unknown>)) {
|
|
39
|
+
if (!Array.isArray(members)) continue;
|
|
40
|
+
teams[name] = members.filter((member): member is string => typeof member === "string");
|
|
42
41
|
}
|
|
43
42
|
return teams;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
function parseAgentFile(filePath: string): AgentDef | null {
|
|
47
46
|
const raw = readFileSync(filePath, "utf-8");
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const frontmatter: Record<string, string> = {};
|
|
52
|
-
for (const line of match[1].split("\n")) {
|
|
53
|
-
const idx = line.indexOf(":");
|
|
54
|
-
if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
55
|
-
}
|
|
56
|
-
if (!frontmatter.name) return null;
|
|
47
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(raw);
|
|
48
|
+
const name = stringField(frontmatter.name);
|
|
49
|
+
if (!name) return null;
|
|
57
50
|
|
|
58
51
|
return {
|
|
59
|
-
name
|
|
60
|
-
description: frontmatter.description
|
|
61
|
-
tools: frontmatter.tools || "read,grep,find,ls",
|
|
62
|
-
systemPrompt:
|
|
52
|
+
name,
|
|
53
|
+
description: stringField(frontmatter.description),
|
|
54
|
+
tools: arrayField(frontmatter.tools).join(",") || "read,grep,find,ls",
|
|
55
|
+
systemPrompt: body,
|
|
63
56
|
file: filePath,
|
|
64
57
|
};
|
|
65
58
|
}
|
|
66
59
|
|
|
60
|
+
function writeSystemPromptFile(agentKey: string, systemPrompt: string): { dir: string; filePath: string } {
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), "openpi-team-"));
|
|
62
|
+
const filePath = join(dir, `system-${agentKey.replace(/[^\w.-]+/g, "_")}.md`);
|
|
63
|
+
writeFileSync(filePath, systemPrompt, { encoding: "utf-8", mode: 0o600 });
|
|
64
|
+
return { dir, filePath };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
67
|
function scanAgentDirs(cwd: string): AgentDef[] {
|
|
68
68
|
const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
|
|
69
69
|
const agents: AgentDef[] = [];
|
|
@@ -161,13 +161,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
161
161
|
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
|
|
162
162
|
const agentKey = state.def.name.toLowerCase().replace(/\s+/g, "-");
|
|
163
163
|
const agentSessionFile = join(sessionDir, `${agentKey}.json`);
|
|
164
|
+
const systemPrompt = writeSystemPromptFile(agentKey, state.def.systemPrompt);
|
|
164
165
|
const args = [
|
|
165
166
|
"--mode", "json",
|
|
166
167
|
"-p",
|
|
167
168
|
"--no-extensions",
|
|
168
169
|
"--tools", state.def.tools,
|
|
169
170
|
"--thinking", "off",
|
|
170
|
-
"--append-system-prompt",
|
|
171
|
+
"--append-system-prompt", systemPrompt.filePath,
|
|
171
172
|
"--session", agentSessionFile,
|
|
172
173
|
];
|
|
173
174
|
if (model) args.splice(4, 0, "--model", model);
|
|
@@ -178,6 +179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
178
179
|
const chunks: string[] = [];
|
|
179
180
|
|
|
180
181
|
return new Promise((resolveDone) => {
|
|
182
|
+
const cleanupPrompt = () => rmSync(systemPrompt.dir, { recursive: true, force: true });
|
|
181
183
|
const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
|
|
182
184
|
const timer = setInterval(() => {
|
|
183
185
|
state.elapsed = Date.now() - start;
|
|
@@ -206,6 +208,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
206
208
|
proc.stderr!.on("data", () => {});
|
|
207
209
|
proc.on("close", (code) => {
|
|
208
210
|
clearInterval(timer);
|
|
211
|
+
cleanupPrompt();
|
|
209
212
|
state.elapsed = Date.now() - start;
|
|
210
213
|
state.status = code === 0 ? "done" : "error";
|
|
211
214
|
if (code === 0) state.sessionFile = agentSessionFile;
|
|
@@ -214,6 +217,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
214
217
|
});
|
|
215
218
|
proc.on("error", (error) => {
|
|
216
219
|
clearInterval(timer);
|
|
220
|
+
cleanupPrompt();
|
|
217
221
|
state.status = "error";
|
|
218
222
|
state.lastWork = error.message;
|
|
219
223
|
updateWidget();
|
package/extensions/commands.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { bundledPromptsDir } from "./lib/packagePaths.ts";
|
|
5
|
+
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
5
6
|
|
|
6
7
|
type CommandDef = {
|
|
7
8
|
name: string;
|
|
@@ -12,42 +13,6 @@ type CommandDef = {
|
|
|
12
13
|
source: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
function parseFrontmatter(raw: string): { fields: Record<string, string | string[]>; body: string } {
|
|
16
|
-
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
17
|
-
if (!match) return { fields: {}, body: raw };
|
|
18
|
-
|
|
19
|
-
const fields: Record<string, string | string[]> = {};
|
|
20
|
-
let currentListKey = "";
|
|
21
|
-
for (const line of match[1].split("\n")) {
|
|
22
|
-
const listMatch = line.match(/^\s+-\s+(.+)$/);
|
|
23
|
-
if (listMatch && currentListKey) {
|
|
24
|
-
const existing = fields[currentListKey];
|
|
25
|
-
fields[currentListKey] = Array.isArray(existing) ? [...existing, listMatch[1].trim()] : [listMatch[1].trim()];
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const idx = line.indexOf(":");
|
|
30
|
-
if (idx > 0) {
|
|
31
|
-
const key = line.slice(0, idx).trim();
|
|
32
|
-
const value = line.slice(idx + 1).trim();
|
|
33
|
-
currentListKey = key;
|
|
34
|
-
fields[key] = value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { fields, body: match[2].trim() };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function stringField(value: string | string[] | undefined): string {
|
|
42
|
-
return typeof value === "string" ? value : "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function arrayField(value: string | string[] | undefined): string[] {
|
|
46
|
-
if (Array.isArray(value)) return value;
|
|
47
|
-
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
16
|
function expandArgs(template: string, args: string): string {
|
|
52
17
|
const parts = args.split(/\s+/).filter(Boolean);
|
|
53
18
|
let result = template.replace(/\$ARGUMENTS|\$@/g, args);
|
|
@@ -64,13 +29,13 @@ function scanPromptDir(dir: string, source: string): CommandDef[] {
|
|
|
64
29
|
for (const file of readdirSync(dir)) {
|
|
65
30
|
if (!file.endsWith(".md")) continue;
|
|
66
31
|
const raw = readFileSync(join(dir, file), "utf-8");
|
|
67
|
-
const {
|
|
32
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(raw);
|
|
68
33
|
const firstLine = body.split("\n").find((line) => line.trim())?.trim() || "";
|
|
69
34
|
commands.push({
|
|
70
|
-
name: stringField(
|
|
71
|
-
description: stringField(
|
|
72
|
-
category: stringField(
|
|
73
|
-
aliases: arrayField(
|
|
35
|
+
name: stringField(frontmatter.name) || basename(file, ".md"),
|
|
36
|
+
description: stringField(frontmatter.description) || firstLine.slice(0, 120),
|
|
37
|
+
category: stringField(frontmatter.category) || "general",
|
|
38
|
+
aliases: arrayField(frontmatter.aliases),
|
|
74
39
|
body,
|
|
75
40
|
source,
|
|
76
41
|
});
|
|
@@ -82,8 +47,8 @@ function scanPromptDir(dir: string, source: string): CommandDef[] {
|
|
|
82
47
|
export default function (pi: ExtensionAPI) {
|
|
83
48
|
const cwd = process.cwd();
|
|
84
49
|
const commands = [
|
|
85
|
-
...scanPromptDir(bundledPromptsDir, "openpi"),
|
|
86
50
|
...scanPromptDir(join(cwd, ".pi", "prompts"), ".pi"),
|
|
51
|
+
...scanPromptDir(bundledPromptsDir, "openpi"),
|
|
87
52
|
];
|
|
88
53
|
const byName = new Map<string, CommandDef>();
|
|
89
54
|
const aliases = new Map<string, string>();
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* Usage: pi -e extensions/damage-control-continue.ts
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { ExtensionAPI } from "@
|
|
16
|
-
import { isToolCallEventType } from "@
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { parse as yamlParse } from "yaml";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
|
|
3
|
+
export type FrontmatterValue = string | string[] | number | boolean | null | undefined;
|
|
4
|
+
|
|
5
|
+
export function parseMarkdownFrontmatter<T extends Record<string, FrontmatterValue> = Record<string, FrontmatterValue>>(
|
|
6
|
+
raw: string,
|
|
7
|
+
): { frontmatter: T; body: string } {
|
|
8
|
+
const match = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$/);
|
|
9
|
+
if (!match) return { frontmatter: {} as T, body: raw };
|
|
10
|
+
|
|
11
|
+
const parsed = parseYaml(match[1]);
|
|
12
|
+
const frontmatter = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
13
|
+
? parsed as T
|
|
14
|
+
: {} as T;
|
|
15
|
+
|
|
16
|
+
return { frontmatter, body: match[2].trim() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function stringField(value: FrontmatterValue): string {
|
|
20
|
+
return typeof value === "string" ? value : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function arrayField(value: FrontmatterValue): string[] {
|
|
24
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
|
|
25
|
+
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
package/extensions/minimal.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* Usage: pi -e extensions/minimal.ts
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ExtensionAPI } from "@
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
11
|
-
import { truncateToWidth, visibleWidth } from "@
|
|
11
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
13
|
export default function (pi: ExtensionAPI) {
|
|
14
14
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -31,4 +31,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
31
|
},
|
|
32
32
|
}));
|
|
33
33
|
});
|
|
34
|
-
}
|
|
34
|
+
}
|
package/extensions/openpi.ts
CHANGED
package/extensions/pure-focus.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Usage: pi -e examples/extensions/pure-focus.ts
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionAPI } from "@
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
12
12
|
|
|
13
13
|
export default function (pi: ExtensionAPI) {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Usage: pi -e extensions/purpose-gate.ts
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { ExtensionAPI } from "@
|
|
12
|
-
import { Text, truncateToWidth } from "@
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
14
14
|
|
|
15
15
|
// synthwave: bgWarm #4a1e6a → rgb(74,30,106)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { bundledAgentsDir, bundledPiPiAgentsDir } from "./lib/packagePaths.ts";
|
|
6
|
+
import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
|
|
6
7
|
|
|
7
8
|
type AgentDef = {
|
|
8
9
|
name: string;
|
|
@@ -12,28 +13,17 @@ type AgentDef = {
|
|
|
12
13
|
source: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
function parseFrontmatter(raw: string): { fields: Record<string, string>; body: string } {
|
|
16
|
-
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
17
|
-
if (!match) return { fields: {}, body: raw };
|
|
18
|
-
const fields: Record<string, string> = {};
|
|
19
|
-
for (const line of match[1].split("\n")) {
|
|
20
|
-
const idx = line.indexOf(":");
|
|
21
|
-
if (idx > 0) fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
22
|
-
}
|
|
23
|
-
return { fields, body: match[2] };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
function scanAgents(dir: string, source: string): AgentDef[] {
|
|
27
17
|
if (!existsSync(dir)) return [];
|
|
28
18
|
const agents: AgentDef[] = [];
|
|
29
19
|
for (const file of readdirSync(dir)) {
|
|
30
20
|
if (!file.endsWith(".md")) continue;
|
|
31
21
|
const raw = readFileSync(join(dir, file), "utf-8");
|
|
32
|
-
const {
|
|
22
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(raw);
|
|
33
23
|
agents.push({
|
|
34
|
-
name:
|
|
35
|
-
description:
|
|
36
|
-
tools:
|
|
24
|
+
name: stringField(frontmatter.name) || basename(file, ".md"),
|
|
25
|
+
description: stringField(frontmatter.description),
|
|
26
|
+
tools: arrayField(frontmatter.tools),
|
|
37
27
|
body: body.trim(),
|
|
38
28
|
source,
|
|
39
29
|
});
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* Usage: pi -e extensions/theme-cycler.ts -e extensions/minimal.ts
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
21
|
-
import { truncateToWidth } from "@
|
|
20
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
22
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
23
23
|
|
|
24
24
|
export default function (pi: ExtensionAPI) {
|
package/extensions/themeMap.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* synthwave · tokyo-night
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { ExtensionContext } from "@
|
|
14
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { basename } from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Usage: pi -e extensions/tool-counter-widget.ts
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionAPI } from "@
|
|
11
|
-
import { Box, Text } from "@
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
13
13
|
|
|
14
14
|
const palette = [
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* Usage: pi -e extensions/tool-counter.ts
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { AssistantMessage } from "@
|
|
14
|
-
import type { ExtensionAPI } from "@
|
|
15
|
-
import { truncateToWidth, visibleWidth } from "@
|
|
13
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
16
|
import { basename } from "node:path";
|
|
17
17
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matyah00/openpi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi-native commands, skills, agents, and workflows.",
|
|
6
6
|
"keywords": [
|
|
@@ -24,16 +24,24 @@
|
|
|
24
24
|
"agents",
|
|
25
25
|
"extensions",
|
|
26
26
|
"prompts",
|
|
27
|
+
"scripts",
|
|
27
28
|
"skills",
|
|
28
29
|
"themes",
|
|
29
30
|
"types",
|
|
30
31
|
"damage-control-rules.yaml",
|
|
32
|
+
"LICENSE",
|
|
31
33
|
"README.md",
|
|
32
34
|
"tsconfig.json"
|
|
33
35
|
],
|
|
34
36
|
"publishConfig": {
|
|
35
37
|
"access": "public"
|
|
36
38
|
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"check": "npm run validate && npm run typecheck && npm run pack:check",
|
|
41
|
+
"pack:check": "npm pack --dry-run",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"validate": "node scripts/validate-package.mjs"
|
|
44
|
+
},
|
|
37
45
|
"dependencies": {
|
|
38
46
|
"yaml": "^2.8.0"
|
|
39
47
|
},
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
|
|
6
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
7
|
+
const failures = [];
|
|
8
|
+
|
|
9
|
+
function fail(message) {
|
|
10
|
+
failures.push(message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function read(path) {
|
|
14
|
+
return readFileSync(join(root, path), "utf-8");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walk(dir, predicate = () => true) {
|
|
18
|
+
const absolute = join(root, dir);
|
|
19
|
+
if (!existsSync(absolute)) return [];
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of readdirSync(absolute, { withFileTypes: true })) {
|
|
22
|
+
const full = join(absolute, entry.name);
|
|
23
|
+
if (entry.isDirectory()) files.push(...walk(relative(root, full), predicate));
|
|
24
|
+
else if (predicate(entry.name)) files.push(relative(root, full).replace(/\\/g, "/"));
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseMarkdown(file) {
|
|
30
|
+
const raw = read(file);
|
|
31
|
+
const match = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$/);
|
|
32
|
+
if (!match) return { frontmatter: {}, body: raw };
|
|
33
|
+
const frontmatter = parseYaml(match[1]) ?? {};
|
|
34
|
+
if (!frontmatter || typeof frontmatter !== "object" || Array.isArray(frontmatter)) {
|
|
35
|
+
fail(`${file}: frontmatter must be a mapping`);
|
|
36
|
+
return { frontmatter: {}, body: match[2] };
|
|
37
|
+
}
|
|
38
|
+
return { frontmatter, body: match[2] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const file of walk("extensions", (name) => name.endsWith(".ts"))) {
|
|
42
|
+
const content = read(file);
|
|
43
|
+
if (content.includes("@mariozechner/")) fail(`${file}: uses old @mariozechner namespace`);
|
|
44
|
+
if (content.includes("@sinclair/typebox")) fail(`${file}: uses @sinclair/typebox instead of typebox`);
|
|
45
|
+
if (/--append-system-prompt",\s*(state\.def|agentDef)\.systemPrompt/.test(content)) {
|
|
46
|
+
fail(`${file}: passes raw system prompt instead of a temp file`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const promptNames = new Map();
|
|
51
|
+
for (const file of walk("prompts", (name) => name.endsWith(".md"))) {
|
|
52
|
+
const { frontmatter, body } = parseMarkdown(file);
|
|
53
|
+
const name = typeof frontmatter.name === "string" ? frontmatter.name : basename(file, ".md");
|
|
54
|
+
if (!body.trim()) fail(`${file}: prompt body is empty`);
|
|
55
|
+
if (promptNames.has(name)) fail(`${file}: duplicate prompt name "${name}" also used by ${promptNames.get(name)}`);
|
|
56
|
+
promptNames.set(name, file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agentNames = new Set();
|
|
60
|
+
for (const file of walk("agents", (name) => name.endsWith(".md"))) {
|
|
61
|
+
const { frontmatter, body } = parseMarkdown(file);
|
|
62
|
+
if (typeof frontmatter.name !== "string" || !frontmatter.name.trim()) fail(`${file}: missing agent name`);
|
|
63
|
+
if (typeof frontmatter.description !== "string" || !frontmatter.description.trim()) fail(`${file}: missing agent description`);
|
|
64
|
+
if (!body.trim()) fail(`${file}: agent body is empty`);
|
|
65
|
+
if (typeof frontmatter.name === "string") agentNames.add(frontmatter.name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const teams = parseYaml(read("agents/teams.yaml")) ?? {};
|
|
69
|
+
for (const [team, members] of Object.entries(teams)) {
|
|
70
|
+
if (!Array.isArray(members)) {
|
|
71
|
+
fail(`agents/teams.yaml: ${team} must be a list`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const member of members) {
|
|
75
|
+
if (!agentNames.has(member)) fail(`agents/teams.yaml: ${team} references unknown agent ${member}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const chains = parseYaml(read("agents/agent-chain.yaml")) ?? {};
|
|
80
|
+
for (const [chainName, chain] of Object.entries(chains)) {
|
|
81
|
+
if (!chain || typeof chain !== "object" || Array.isArray(chain) || !Array.isArray(chain.steps)) {
|
|
82
|
+
fail(`agents/agent-chain.yaml: ${chainName} must have steps`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const [index, step] of chain.steps.entries()) {
|
|
86
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
87
|
+
fail(`agents/agent-chain.yaml: ${chainName} step ${index + 1} must be a mapping`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!agentNames.has(step.agent)) fail(`agents/agent-chain.yaml: ${chainName} references unknown agent ${step.agent}`);
|
|
91
|
+
if (typeof step.prompt !== "string" || !step.prompt.trim()) fail(`agents/agent-chain.yaml: ${chainName} step ${index + 1} missing prompt`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (failures.length) {
|
|
96
|
+
console.error(failures.map((item) => `- ${item}`).join("\n"));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log("openpi package validation passed");
|
package/types/pi-shims.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
declare module "@mariozechner/pi-coding-agent" {
|
|
2
|
-
export type ExtensionContext = any;
|
|
3
|
-
export type ExtensionAPI = any;
|
|
4
|
-
export const DynamicBorder: any;
|
|
5
|
-
export function isToolCallEventType(...args: any[]): boolean;
|
|
6
|
-
export function getMarkdownTheme(...args: any[]): any;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
declare module "@earendil-works/pi-coding-agent" {
|
|
10
2
|
export type ExtensionContext = any;
|
|
11
3
|
export type ExtensionAPI = any;
|
|
@@ -17,36 +9,6 @@ declare module "@earendil-works/pi-coding-agent" {
|
|
|
17
9
|
export function withFileMutationQueue<T = any>(...args: any[]): Promise<T>;
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
declare module "@mariozechner/pi-tui" {
|
|
21
|
-
export class Text {
|
|
22
|
-
constructor(...args: any[]);
|
|
23
|
-
setText(...args: any[]): any;
|
|
24
|
-
render(...args: any[]): any;
|
|
25
|
-
invalidate(...args: any[]): any;
|
|
26
|
-
}
|
|
27
|
-
export class Box {
|
|
28
|
-
constructor(...args: any[]);
|
|
29
|
-
}
|
|
30
|
-
export class Markdown {
|
|
31
|
-
constructor(...args: any[]);
|
|
32
|
-
}
|
|
33
|
-
export class Container {
|
|
34
|
-
constructor(...args: any[]);
|
|
35
|
-
addChild(...args: any[]): any;
|
|
36
|
-
render(...args: any[]): any;
|
|
37
|
-
invalidate(...args: any[]): any;
|
|
38
|
-
}
|
|
39
|
-
export class Spacer {
|
|
40
|
-
constructor(...args: any[]);
|
|
41
|
-
}
|
|
42
|
-
export type AutocompleteItem = any;
|
|
43
|
-
export const Key: any;
|
|
44
|
-
export function matchesKey(...args: any[]): any;
|
|
45
|
-
export function truncateToWidth(...args: any[]): any;
|
|
46
|
-
export function visibleWidth(...args: any[]): any;
|
|
47
|
-
export function getMarkdownTheme(...args: any[]): any;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
12
|
declare module "@earendil-works/pi-tui" {
|
|
51
13
|
export class Text {
|
|
52
14
|
constructor(...args: any[]);
|
|
@@ -77,16 +39,6 @@ declare module "@earendil-works/pi-tui" {
|
|
|
77
39
|
export function getMarkdownTheme(...args: any[]): any;
|
|
78
40
|
}
|
|
79
41
|
|
|
80
|
-
declare module "@sinclair/typebox" {
|
|
81
|
-
export const Type: any;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
declare module "@mariozechner/pi-ai" {
|
|
85
|
-
export type AssistantMessage = any;
|
|
86
|
-
export type Message = any;
|
|
87
|
-
export function StringEnum(...args: any[]): any;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
42
|
declare module "@earendil-works/pi-ai" {
|
|
91
43
|
export type AssistantMessage = any;
|
|
92
44
|
export type Message = any;
|