pi-open-agents 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.
- package/ATTRIBUTION.md +38 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/index.ts +8 -0
- package/package.json +61 -0
- package/src/config/frontmatter.ts +247 -0
- package/src/config/paths.ts +120 -0
- package/src/config/schema.ts +313 -0
- package/src/discovery/loader.ts +180 -0
- package/src/index.ts +204 -0
- package/src/permission/evaluator.ts +160 -0
- package/src/permission/matcher.ts +99 -0
- package/src/permission/parser.ts +81 -0
- package/src/primary/executor.ts +167 -0
- package/src/primary/manager.ts +147 -0
- package/src/subagent/env.ts +116 -0
- package/src/subagent/executor.ts +679 -0
- package/src/subagent/prompt.ts +79 -0
- package/src/subagent/render.ts +251 -0
- package/src/subagent/skills.ts +274 -0
- package/src/subagent/tool.ts +299 -0
- package/src/tui/banner.ts +49 -0
- package/src/tui/selector.ts +331 -0
- package/src/tui/tools.ts +147 -0
- package/src/types.ts +152 -0
- package/src/utils/format.ts +99 -0
package/ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Attribution
|
|
2
|
+
|
|
3
|
+
This project builds on ideas and code from the following open-source projects.
|
|
4
|
+
Significant portions of the initial implementation are derived from these sources.
|
|
5
|
+
|
|
6
|
+
## pi-agent-mode
|
|
7
|
+
|
|
8
|
+
- **Source**: https://github.com/ZGltYQ/agent-mode
|
|
9
|
+
- **Author**: ZGltYQ
|
|
10
|
+
- **License**: MIT
|
|
11
|
+
- **Version at time of fork**: v1.3.0 (commit `751abef`)
|
|
12
|
+
- **What we use**: Primary agent loading, inline system prompt injection,
|
|
13
|
+
model switching, tool whitelist, TUI selector/search UI, status banner,
|
|
14
|
+
`set_agent`/`search_agents` tools, session persistence, `--agent` CLI flag.
|
|
15
|
+
|
|
16
|
+
## pi-subagents
|
|
17
|
+
|
|
18
|
+
- **Source**: https://github.com/jwu/pi-subagents
|
|
19
|
+
- **Author**: jwu (John Wu)
|
|
20
|
+
- **License**: MIT
|
|
21
|
+
- **Version at time of fork**: v2.1.0 (commit `6c568dd`)
|
|
22
|
+
- **What we use**: Subagent execution engine, child session spawning,
|
|
23
|
+
tool scoping, parent-child IPC, TUI rendering for subagent streams,
|
|
24
|
+
`maxDepth`/`allowedAgents` controls, `thinking` frontmatter support.
|
|
25
|
+
|
|
26
|
+
## opencode
|
|
27
|
+
|
|
28
|
+
- **Source**: https://github.com/anomalyco/opencode
|
|
29
|
+
- **Author**: anomalyco (fork of sst/opencode)
|
|
30
|
+
- **Version at time of clone**: commit `cfddb24`
|
|
31
|
+
- **What we reference**: Agent definition format, frontmatter schema,
|
|
32
|
+
discovery paths (`.opencode/agent/`), compatibility target for
|
|
33
|
+
cross-runtime agent definitions.
|
|
34
|
+
|
|
35
|
+
## License compliance
|
|
36
|
+
|
|
37
|
+
Each source file derived from these projects retains appropriate attribution
|
|
38
|
+
in its header comments. The upstream licenses apply to their respective code.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrea Tomassi
|
|
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
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# pi-open-agents
|
|
2
|
+
|
|
3
|
+
Unified agent and subagent management for [pi](https://github.com/earendil-works/pi-coding-agent), with [OpenCode](https://github.com/sst/opencode)-compatible agent definitions.
|
|
4
|
+
|
|
5
|
+
Replaces `pi-agent-mode` and `@johnnywu/pi-subagents` with a single, coherent system.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Pi delegates agent management to two separate npm plugins that use incompatible frontmatter schemas, divergent defaults, and fragmented semantics. This plugin replaces both with one unified system that also reads OpenCode-format agent definitions.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Unified agent definitions** — one `.md` file format, superset of OpenCode + pi
|
|
14
|
+
- **Mode-based visibility** — `primary` agents in the TUI selector, `subagent` agents for delegation, `all` for both
|
|
15
|
+
- **OpenCode-compatible** — reads agents from `.opencode/agent/`, `.opencode/mode/`, and `.agents/`
|
|
16
|
+
- **Permission system** — OpenCode-style `allow`/`deny`/`ask` with glob patterns, last-match-wins
|
|
17
|
+
- **Thinking level per-agent** — `off`/`minimal`/`low`/`medium`/`high`/`xhigh`, with fallback to `settings.json`
|
|
18
|
+
- **Subagent execution** — child pi process spawning with scoped tools, skills, and session fork
|
|
19
|
+
- **Skill loading** — per-agent skills with wildcard support (`lark-*`, `github`)
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install pi-open-agents
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then remove the old plugins from `~/.pi/agent/settings.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"packages": [
|
|
32
|
+
"pi-open-agents"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Agent Definition Format
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
---
|
|
41
|
+
# === Identity ===
|
|
42
|
+
name: my-agent # required
|
|
43
|
+
description: When to use this agent
|
|
44
|
+
mode: primary|subagent|all # default: all
|
|
45
|
+
hidden: false # hide from TUI selectors
|
|
46
|
+
color: "#44BA81"
|
|
47
|
+
|
|
48
|
+
# === Model ===
|
|
49
|
+
model: provider/model-id # per-agent override
|
|
50
|
+
variant: thinking # model variant
|
|
51
|
+
temperature: 0.7
|
|
52
|
+
top_p: 0.95
|
|
53
|
+
|
|
54
|
+
# === Execution ===
|
|
55
|
+
thinking: xhigh # off|minimal|low|medium|high|xhigh
|
|
56
|
+
steps: 50 # max agentic iterations
|
|
57
|
+
systemPrompt: append|replace|replace-all # default: append
|
|
58
|
+
|
|
59
|
+
# === Permissions (OpenCode-style) ===
|
|
60
|
+
permission:
|
|
61
|
+
bash: allow
|
|
62
|
+
edit:
|
|
63
|
+
"*.env": deny
|
|
64
|
+
"*.md": allow
|
|
65
|
+
read: allow
|
|
66
|
+
|
|
67
|
+
# === Subagent controls ===
|
|
68
|
+
maxDepth: 10 # recursion limit
|
|
69
|
+
allowedAgents: worker, explorer # which subagents this can spawn
|
|
70
|
+
skills: lark-*, github # per-agent skills with wildcards
|
|
71
|
+
---
|
|
72
|
+
You are a specialized agent. Your markdown body becomes the system prompt.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Discovery Paths
|
|
76
|
+
|
|
77
|
+
Agents are loaded from multiple locations (project overrides global by name):
|
|
78
|
+
|
|
79
|
+
| Path | Scope | Format |
|
|
80
|
+
|------|-------|--------|
|
|
81
|
+
| `~/.pi/agent/agents/*.md` | Global | pi |
|
|
82
|
+
| `~/.opencode/{agent,agents,mode}/*.md` | Global | OpenCode |
|
|
83
|
+
| `.pi/agents/*.md` | Project | pi |
|
|
84
|
+
| `.opencode/{agent,agents,mode}/*.md` | Project | OpenCode |
|
|
85
|
+
| `.agents/*.md` | Project | Shared |
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Interactive
|
|
90
|
+
|
|
91
|
+
| Action | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `/agent` | Open agent selector (primary/all only) |
|
|
94
|
+
| `/agent <name>` | Switch to agent directly |
|
|
95
|
+
| `/agents` | List all agents |
|
|
96
|
+
| `/agent-search <query>` | Search agents |
|
|
97
|
+
| `Ctrl+Shift+M` | Cycle agents |
|
|
98
|
+
| `Alt+S` | Search prompt |
|
|
99
|
+
| `--agent <name>` | CLI flag for startup agent |
|
|
100
|
+
|
|
101
|
+
### Programmatic (LLM tools)
|
|
102
|
+
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `set_agent` | Switch agent programmatically |
|
|
106
|
+
| `search_agents` | Search agents by name/description/body |
|
|
107
|
+
| `subagent` | Delegate task to a subagent (subagent/all mode only) |
|
|
108
|
+
|
|
109
|
+
## Migration from pi-agent-mode + pi-subagents
|
|
110
|
+
|
|
111
|
+
1. Install: `pi install pi-open-agents`
|
|
112
|
+
2. Remove `npm:pi-agent-mode` and `npm:@johnnywu/pi-subagents` from settings
|
|
113
|
+
3. Existing agent `.md` files work without changes (default `mode: all`)
|
|
114
|
+
4. Gradually add `mode: primary` or `mode: subagent` to control visibility
|
|
115
|
+
5. Gradually adopt `permission:` over `tools:` whitelist
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install
|
|
121
|
+
npm test # 84 tests
|
|
122
|
+
npm run typecheck # tsc --noEmit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Attribution
|
|
126
|
+
|
|
127
|
+
This project builds on code and ideas from:
|
|
128
|
+
|
|
129
|
+
- [pi-agent-mode](https://github.com/ZGltYQ/agent-mode) by ZGltYQ (MIT)
|
|
130
|
+
- [pi-subagents](https://github.com/jwu/pi-subagents) by jwu (MIT)
|
|
131
|
+
- [opencode](https://github.com/sst/opencode) by sst (MIT)
|
|
132
|
+
|
|
133
|
+
See `ATTRIBUTION.md` for details.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-open-agents",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unified agent and subagent management for pi coding agent, with OpenCode-compatible agent definitions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"extension",
|
|
10
|
+
"agents",
|
|
11
|
+
"subagents",
|
|
12
|
+
"opencode"
|
|
13
|
+
],
|
|
14
|
+
"author": "Andrea Tomassi",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/andrea-tomassi/pi-open-agents.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/andrea-tomassi/pi-open-agents/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/andrea-tomassi/pi-open-agents#readme",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.ts",
|
|
30
|
+
"src/",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"ATTRIBUTION.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"test": "node --test --import tsx test/**/*.test.ts"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./index.ts"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-ai": "*",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
47
|
+
"@earendil-works/pi-tui": "*",
|
|
48
|
+
"typebox": "*"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@earendil-works/pi-ai": { "optional": true },
|
|
52
|
+
"@earendil-works/pi-coding-agent": { "optional": true },
|
|
53
|
+
"@earendil-works/pi-tui": { "optional": true },
|
|
54
|
+
"typebox": { "optional": true }
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
58
|
+
"tsx": "^4.19.0",
|
|
59
|
+
"typescript": "^5.6.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained YAML frontmatter parser.
|
|
3
|
+
*
|
|
4
|
+
* Handles the subset of YAML used by agent definitions:
|
|
5
|
+
* - Flat key-value pairs
|
|
6
|
+
* - Nested objects (for permission rules)
|
|
7
|
+
* - Type coercion (boolean, number, string)
|
|
8
|
+
* - Quoted values
|
|
9
|
+
*
|
|
10
|
+
* No external dependencies — works in isolation for testing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RawLine {
|
|
14
|
+
indent: number;
|
|
15
|
+
content: string;
|
|
16
|
+
lineNumber: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ParsedFrontmatter {
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
body: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract and parse frontmatter from markdown content.
|
|
26
|
+
*
|
|
27
|
+
* Returns { data: {}, body: content } if no frontmatter is present.
|
|
28
|
+
*/
|
|
29
|
+
export function parseFrontmatter(content: string): ParsedFrontmatter {
|
|
30
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
31
|
+
|
|
32
|
+
// No frontmatter block
|
|
33
|
+
if (!normalized.startsWith("---")) {
|
|
34
|
+
return { data: {}, body: normalized.trim() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find the closing ---
|
|
38
|
+
// Skip the opening --- and the newline after it
|
|
39
|
+
const afterOpening = normalized.slice(3);
|
|
40
|
+
if (!afterOpening.startsWith("\n") && !afterOpening.startsWith("\r")) {
|
|
41
|
+
// --- immediately followed by content (not a frontmatter block)
|
|
42
|
+
return { data: {}, body: normalized.trim() };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const closingIdx = normalized.indexOf("\n---", 3);
|
|
46
|
+
if (closingIdx === -1) {
|
|
47
|
+
// No closing delimiter — treat entire content as body
|
|
48
|
+
return { data: {}, body: normalized.trim() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const yamlString = normalized.slice(4, closingIdx);
|
|
52
|
+
const body = normalized.slice(closingIdx + 4).replace(/^\n/, "").trim();
|
|
53
|
+
|
|
54
|
+
const data = parseYaml(yamlString);
|
|
55
|
+
|
|
56
|
+
return { data, body };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Minimal YAML Parser ─────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse a YAML string into a nested object.
|
|
63
|
+
*
|
|
64
|
+
* Handles:
|
|
65
|
+
* - key: value (flat string/number/boolean)
|
|
66
|
+
* - key: (start of nested object)
|
|
67
|
+
* - subkey: value (nested under parent)
|
|
68
|
+
* - key: [a, b, c] (inline array)
|
|
69
|
+
* - Comments (# ...)
|
|
70
|
+
* - Quoted values
|
|
71
|
+
*/
|
|
72
|
+
function parseYaml(input: string): Record<string, unknown> {
|
|
73
|
+
const lines = preprocessLines(input);
|
|
74
|
+
const root: Record<string, unknown> = {};
|
|
75
|
+
parseBlock(lines, 0, lines.length, 0, root);
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function preprocessLines(input: string): RawLine[] {
|
|
80
|
+
const result: RawLine[] = [];
|
|
81
|
+
const rawLines = input.split("\n");
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
84
|
+
const line = rawLines[i];
|
|
85
|
+
|
|
86
|
+
// Skip empty lines and comments
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
89
|
+
|
|
90
|
+
// Calculate indentation (spaces only, tabs not supported in YAML)
|
|
91
|
+
const indent = line.length - line.trimStart().length;
|
|
92
|
+
|
|
93
|
+
result.push({
|
|
94
|
+
indent,
|
|
95
|
+
content: line.trim(),
|
|
96
|
+
lineNumber: i + 1,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recursively parse a block of lines at a given indentation level.
|
|
105
|
+
*
|
|
106
|
+
* @param lines All preprocessed lines
|
|
107
|
+
* @param start Start index in lines array
|
|
108
|
+
* @param end End index (exclusive)
|
|
109
|
+
* @param indent Expected indentation level for this block
|
|
110
|
+
* @param target Object to populate
|
|
111
|
+
* @returns Number of lines consumed
|
|
112
|
+
*/
|
|
113
|
+
function parseBlock(
|
|
114
|
+
lines: RawLine[],
|
|
115
|
+
start: number,
|
|
116
|
+
end: number,
|
|
117
|
+
indent: number,
|
|
118
|
+
target: Record<string, unknown>,
|
|
119
|
+
): number {
|
|
120
|
+
let i = start;
|
|
121
|
+
|
|
122
|
+
while (i < end) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
|
|
125
|
+
// This line belongs to a shallower scope — we're done with this block
|
|
126
|
+
if (line.indent < indent) break;
|
|
127
|
+
|
|
128
|
+
// Deeper indentation without a parent key — skip (malformed)
|
|
129
|
+
if (line.indent > indent) {
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const colonIdx = line.content.indexOf(":");
|
|
135
|
+
if (colonIdx === -1) {
|
|
136
|
+
// No colon — skip malformed line
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const key = line.content.slice(0, colonIdx).trim().replace(/^['"]|['"]$/g, "");
|
|
142
|
+
const rawValue = line.content.slice(colonIdx + 1).trim();
|
|
143
|
+
|
|
144
|
+
if (!key) {
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (rawValue === "") {
|
|
150
|
+
// Value is empty — check if next lines are nested
|
|
151
|
+
if (i + 1 < end && lines[i + 1].indent > indent) {
|
|
152
|
+
// Parse nested object
|
|
153
|
+
const nested: Record<string, unknown> = {};
|
|
154
|
+
const childIndent = lines[i + 1].indent;
|
|
155
|
+
let consumed = 0;
|
|
156
|
+
let j = i + 1;
|
|
157
|
+
|
|
158
|
+
while (j < end && lines[j].indent >= childIndent) {
|
|
159
|
+
if (lines[j].indent === childIndent) {
|
|
160
|
+
const subColon = lines[j].content.indexOf(":");
|
|
161
|
+
if (subColon === -1) {
|
|
162
|
+
j++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const subKey = lines[j].content.slice(0, subColon).trim().replace(/^['"]|['"]$/g, "");
|
|
166
|
+
const subRaw = lines[j].content.slice(subColon + 1).trim();
|
|
167
|
+
|
|
168
|
+
if (subRaw === "" && j + 1 < end && lines[j + 1].indent > childIndent) {
|
|
169
|
+
// Double nesting (e.g. edit: { "*.env": deny })
|
|
170
|
+
const deepNested: Record<string, unknown> = {};
|
|
171
|
+
const deepIndent = lines[j + 1].indent;
|
|
172
|
+
let deepJ = j + 1;
|
|
173
|
+
while (deepJ < end && lines[deepJ].indent >= deepIndent) {
|
|
174
|
+
if (lines[deepJ].indent === deepIndent) {
|
|
175
|
+
const dColon = lines[deepJ].content.indexOf(":");
|
|
176
|
+
if (dColon !== -1) {
|
|
177
|
+
const dKey = lines[deepJ].content.slice(0, dColon).trim().replace(/^['"]|['"]$/g, "");
|
|
178
|
+
const dVal = lines[deepJ].content.slice(dColon + 1).trim();
|
|
179
|
+
deepNested[dKey] = coerceValue(dVal);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
deepJ++;
|
|
183
|
+
}
|
|
184
|
+
nested[subKey] = deepNested;
|
|
185
|
+
j = deepJ;
|
|
186
|
+
} else {
|
|
187
|
+
nested[subKey] = coerceValue(subRaw);
|
|
188
|
+
j++;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
j++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
target[key] = nested;
|
|
195
|
+
i = j;
|
|
196
|
+
} else {
|
|
197
|
+
// Empty value with no children
|
|
198
|
+
target[key] = "";
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Inline value
|
|
203
|
+
target[key] = coerceValue(rawValue);
|
|
204
|
+
i++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return i - start;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Coerce a raw string value into the appropriate JS type.
|
|
213
|
+
*
|
|
214
|
+
* Handles:
|
|
215
|
+
* - Quoted strings: "hello" or 'hello' → hello
|
|
216
|
+
* - Booleans: true/false
|
|
217
|
+
* - Numbers: 42, 0.7
|
|
218
|
+
* - Inline arrays: [a, b, c]
|
|
219
|
+
* - Everything else: string
|
|
220
|
+
*/
|
|
221
|
+
function coerceValue(raw: string): unknown {
|
|
222
|
+
// Inline array: [a, b, c]
|
|
223
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
224
|
+
const inner = raw.slice(1, -1).trim();
|
|
225
|
+
if (!inner) return [];
|
|
226
|
+
return inner.split(",").map((s) => coerceValue(s.trim()) as string);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Quoted strings — strip quotes, keep inner content
|
|
230
|
+
if (
|
|
231
|
+
(raw.startsWith('"') && raw.endsWith('"')) ||
|
|
232
|
+
(raw.startsWith("'") && raw.endsWith("'"))
|
|
233
|
+
) {
|
|
234
|
+
return raw.slice(1, -1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Booleans
|
|
238
|
+
if (raw === "true") return true;
|
|
239
|
+
if (raw === "false") return false;
|
|
240
|
+
|
|
241
|
+
// Numbers (integer or float)
|
|
242
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
243
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
244
|
+
|
|
245
|
+
// Plain string
|
|
246
|
+
return raw;
|
|
247
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery paths for agent definition files.
|
|
3
|
+
*
|
|
4
|
+
* Scans three path families: pi, OpenCode, and shared.
|
|
5
|
+
* Project paths override global paths by agent name.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
|
|
11
|
+
export interface DiscoveryPath {
|
|
12
|
+
/** Absolute directory path to scan */
|
|
13
|
+
dir: string;
|
|
14
|
+
/** Whether this is a global or project-scoped path */
|
|
15
|
+
source: "global" | "project";
|
|
16
|
+
/** Which path family this belongs to */
|
|
17
|
+
family: "pi" | "opencode" | "shared";
|
|
18
|
+
/**
|
|
19
|
+
* Subdirectory pattern within the dir to scan.
|
|
20
|
+
* If empty, scans the dir directly.
|
|
21
|
+
* Multiple patterns are tried in order.
|
|
22
|
+
*/
|
|
23
|
+
subdirs: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Path Builders ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build all discovery paths for the given cwd and agent dir.
|
|
30
|
+
*
|
|
31
|
+
* @param cwd Current working directory (project root)
|
|
32
|
+
* @param agentDir pi agent directory (~/.pi/agent by default)
|
|
33
|
+
* @returns Ordered list of paths to scan (global first, then project)
|
|
34
|
+
*/
|
|
35
|
+
export function getDiscoveryPaths(cwd: string, agentDir: string): DiscoveryPath[] {
|
|
36
|
+
const home = os.homedir();
|
|
37
|
+
const opencodeHome = path.join(home, ".opencode");
|
|
38
|
+
const opencodeConfig = path.join(home, ".config", "opencode");
|
|
39
|
+
|
|
40
|
+
const paths: DiscoveryPath[] = [];
|
|
41
|
+
|
|
42
|
+
// ── Global paths (scanned first, lower priority) ──
|
|
43
|
+
|
|
44
|
+
// pi global
|
|
45
|
+
paths.push({
|
|
46
|
+
dir: path.join(agentDir, "agents"),
|
|
47
|
+
source: "global",
|
|
48
|
+
family: "pi",
|
|
49
|
+
subdirs: [""],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// OpenCode global (config dir)
|
|
53
|
+
paths.push({
|
|
54
|
+
dir: opencodeConfig,
|
|
55
|
+
source: "global",
|
|
56
|
+
family: "opencode",
|
|
57
|
+
subdirs: ["agent", "agents", "mode", "modes"],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// OpenCode global (home .opencode)
|
|
61
|
+
paths.push({
|
|
62
|
+
dir: opencodeHome,
|
|
63
|
+
source: "global",
|
|
64
|
+
family: "opencode",
|
|
65
|
+
subdirs: ["agent", "agents", "mode", "modes"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Project paths (scanned after global, higher priority) ──
|
|
69
|
+
|
|
70
|
+
// pi project
|
|
71
|
+
paths.push({
|
|
72
|
+
dir: path.join(cwd, ".pi", "agents"),
|
|
73
|
+
source: "project",
|
|
74
|
+
family: "pi",
|
|
75
|
+
subdirs: [""],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// OpenCode project
|
|
79
|
+
paths.push({
|
|
80
|
+
dir: path.join(cwd, ".opencode"),
|
|
81
|
+
source: "project",
|
|
82
|
+
family: "opencode",
|
|
83
|
+
subdirs: ["agent", "agents", "mode", "modes"],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Shared (cross-runtime)
|
|
87
|
+
paths.push({
|
|
88
|
+
dir: path.join(cwd, ".agents"),
|
|
89
|
+
source: "project",
|
|
90
|
+
family: "shared",
|
|
91
|
+
subdirs: [""],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return paths;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Derive the agent name from a file path, using the discovery path info.
|
|
99
|
+
*
|
|
100
|
+
* For pi/shared: uses the filename without extension
|
|
101
|
+
* .pi/agents/my-agent.md → "my-agent"
|
|
102
|
+
*
|
|
103
|
+
* For OpenCode: strips the subdir prefix
|
|
104
|
+
* .opencode/agent/triage.md → "triage"
|
|
105
|
+
* .opencode/agents/deep/code-review.md → "deep-code-review" (flattened)
|
|
106
|
+
*
|
|
107
|
+
* An explicit `name:` in frontmatter always takes precedence.
|
|
108
|
+
*/
|
|
109
|
+
export function deriveAgentName(filePath: string, basePath: string, subdir: string): string {
|
|
110
|
+
const fullPath = subdir ? path.join(basePath, subdir) : basePath;
|
|
111
|
+
const relative = path.relative(fullPath, filePath);
|
|
112
|
+
|
|
113
|
+
// Remove .md extension
|
|
114
|
+
const withoutExt = relative.replace(/\.md$/i, "");
|
|
115
|
+
|
|
116
|
+
// Normalize path separators
|
|
117
|
+
const normalized = withoutExt.replace(/[/\\]/g, "-");
|
|
118
|
+
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|