@nmzpy/pi-ember-stack 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 +9 -0
- package/README.md +63 -0
- package/package.json +50 -0
- package/src/pi-ember-stack.ts +983 -0
- package/src/questionnaire-tool.ts +287 -0
- package/src/subagent/LICENSE +21 -0
- package/src/subagent/README.md +52 -0
- package/src/subagent/agent-format.md +64 -0
- package/src/subagent/agents/architect.md +56 -0
- package/src/subagent/agents/coder.md +35 -0
- package/src/subagent/agents/general-purpose.md +13 -0
- package/src/subagent/agents/reviewer.md +36 -0
- package/src/subagent/agents/scout.md +44 -0
- package/src/subagent/agents/worker.md +15 -0
- package/src/subagent/extensions/agents.ts +223 -0
- package/src/subagent/extensions/index.ts +1218 -0
- package/src/subagent/extensions/model.ts +86 -0
- package/src/subagent/extensions/package.json +3 -0
- package/src/subagent/extensions/render.ts +278 -0
- package/src/subagent/extensions/runner.ts +317 -0
- package/src/subagent/extensions/service.ts +79 -0
- package/src/subagent/extensions/thread-viewer.ts +393 -0
- package/src/subagent/extensions/threads.ts +116 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration for pi-subagent.
|
|
3
|
+
*
|
|
4
|
+
* Loads agent definitions from Markdown files with YAML frontmatter.
|
|
5
|
+
* Discovers from user-level (~/.pi/agent/agents/), project-level
|
|
6
|
+
* (.pi/agents/), and bundled skill agents. Results are cached and
|
|
7
|
+
* invalidated on /reload.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
export type AgentScope = "user" | "project" | "both";
|
|
15
|
+
|
|
16
|
+
export interface AgentConfig {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
tools?: string[];
|
|
20
|
+
model?: string;
|
|
21
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
source: "user" | "project" | "bundled";
|
|
24
|
+
filePath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentDiscoveryResult {
|
|
28
|
+
agents: AgentConfig[];
|
|
29
|
+
projectAgentsDir: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AgentCache {
|
|
33
|
+
userDir: string;
|
|
34
|
+
projectDir: string | null;
|
|
35
|
+
bundledDir: string;
|
|
36
|
+
scope: AgentScope;
|
|
37
|
+
agents: AgentConfig[];
|
|
38
|
+
projectAgentsDir: string | null;
|
|
39
|
+
/** File-level signature per directory (name:mtime:size for each .md file) */
|
|
40
|
+
dirSignatures: Map<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let _cache: AgentCache | null = null;
|
|
44
|
+
|
|
45
|
+
/** Clear the agent cache (call on /reload). */
|
|
46
|
+
export function invalidateAgentCache(): void {
|
|
47
|
+
_cache = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"): AgentConfig[] {
|
|
51
|
+
const agents: AgentConfig[] = [];
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(dir)) return agents;
|
|
54
|
+
|
|
55
|
+
let entries: fs.Dirent[];
|
|
56
|
+
try {
|
|
57
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
58
|
+
} catch {
|
|
59
|
+
return agents;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
64
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
65
|
+
|
|
66
|
+
const filePath = path.join(dir, entry.name);
|
|
67
|
+
let content: string;
|
|
68
|
+
try {
|
|
69
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
75
|
+
|
|
76
|
+
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") continue;
|
|
77
|
+
|
|
78
|
+
const tools =
|
|
79
|
+
typeof frontmatter.tools === "string"
|
|
80
|
+
? frontmatter.tools.split(",").map((t) => t.trim()).filter(Boolean)
|
|
81
|
+
: Array.isArray(frontmatter.tools)
|
|
82
|
+
? (frontmatter.tools as unknown[]).filter((t): t is string => typeof t === "string")
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
agents.push({
|
|
86
|
+
name: frontmatter.name,
|
|
87
|
+
description: frontmatter.description,
|
|
88
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
89
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
90
|
+
thinking: typeof frontmatter.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(frontmatter.thinking)
|
|
91
|
+
? frontmatter.thinking as AgentConfig["thinking"]
|
|
92
|
+
: undefined,
|
|
93
|
+
systemPrompt: body,
|
|
94
|
+
source,
|
|
95
|
+
filePath,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return agents;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isDirectory(p: string): boolean {
|
|
103
|
+
try {
|
|
104
|
+
return fs.statSync(p).isDirectory();
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Build a stable signature for agent .md files in a directory.
|
|
111
|
+
* Returns "missing" if the directory doesn't exist, or a sorted
|
|
112
|
+
* list of `name:mtimeMs:size` entries that catches both content
|
|
113
|
+
* edits and add/remove/rename operations. */
|
|
114
|
+
function dirSignature(dir: string): string {
|
|
115
|
+
try {
|
|
116
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
117
|
+
.filter((e) => e.name.endsWith(".md") && (e.isFile() || e.isSymbolicLink()))
|
|
118
|
+
.map((e) => {
|
|
119
|
+
const file = path.join(dir, e.name);
|
|
120
|
+
const st = fs.statSync(file);
|
|
121
|
+
return `${e.name}:${st.mtimeMs}:${st.size}`;
|
|
122
|
+
})
|
|
123
|
+
.sort();
|
|
124
|
+
return `exists:${entries.join("|")}`;
|
|
125
|
+
} catch {
|
|
126
|
+
return "missing";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
131
|
+
let currentDir = cwd;
|
|
132
|
+
while (true) {
|
|
133
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
134
|
+
if (isDirectory(candidate)) return candidate;
|
|
135
|
+
|
|
136
|
+
const parentDir = path.dirname(currentDir);
|
|
137
|
+
if (parentDir === currentDir) return null;
|
|
138
|
+
currentDir = parentDir;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Discover agents from standard locations plus bundled skill agents.
|
|
144
|
+
* @param cwd - Working directory for project-level discovery.
|
|
145
|
+
* @param scope - Which agent directories to use.
|
|
146
|
+
* @param bundledAgentsDir - Path to skill-bundled agents directory.
|
|
147
|
+
*/
|
|
148
|
+
export function discoverAgents(
|
|
149
|
+
cwd: string,
|
|
150
|
+
scope: AgentScope,
|
|
151
|
+
bundledAgentsDir: string,
|
|
152
|
+
): AgentDiscoveryResult {
|
|
153
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
154
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
155
|
+
|
|
156
|
+
// Check cache (with file-signature invalidation so editing agent .md files auto-detects changes)
|
|
157
|
+
if (
|
|
158
|
+
_cache &&
|
|
159
|
+
_cache.userDir === userDir &&
|
|
160
|
+
_cache.projectDir === projectAgentsDir &&
|
|
161
|
+
_cache.bundledDir === bundledAgentsDir &&
|
|
162
|
+
_cache.scope === scope
|
|
163
|
+
) {
|
|
164
|
+
let stale = false;
|
|
165
|
+
for (const [dir, cachedSig] of _cache.dirSignatures) {
|
|
166
|
+
if (dirSignature(dir) !== cachedSig) {
|
|
167
|
+
stale = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!stale) {
|
|
172
|
+
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir };
|
|
173
|
+
}
|
|
174
|
+
// Cache is stale — rebuild below
|
|
175
|
+
_cache = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
179
|
+
const projectAgents =
|
|
180
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
181
|
+
const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled");
|
|
182
|
+
|
|
183
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
184
|
+
|
|
185
|
+
// Priority: bundled < user < project (higher index = higher priority)
|
|
186
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
187
|
+
if (scope === "both" || scope === "user") {
|
|
188
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
189
|
+
}
|
|
190
|
+
if (scope === "both" || scope === "project") {
|
|
191
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const agents = Array.from(agentMap.values());
|
|
195
|
+
|
|
196
|
+
const dirSignatures = new Map<string, string>();
|
|
197
|
+
for (const dir of [userDir, projectAgentsDir, bundledAgentsDir]) {
|
|
198
|
+
if (!dir) continue;
|
|
199
|
+
dirSignatures.set(dir, dirSignature(dir));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_cache = {
|
|
203
|
+
userDir,
|
|
204
|
+
projectDir: projectAgentsDir,
|
|
205
|
+
bundledDir: bundledAgentsDir,
|
|
206
|
+
scope,
|
|
207
|
+
agents,
|
|
208
|
+
projectAgentsDir,
|
|
209
|
+
dirSignatures,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return { agents, projectAgentsDir };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
216
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
217
|
+
const listed = agents.slice(0, maxItems);
|
|
218
|
+
const remaining = agents.length - listed.length;
|
|
219
|
+
return {
|
|
220
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
221
|
+
remaining,
|
|
222
|
+
};
|
|
223
|
+
}
|