@bacnh85/pi-subagent 0.10.1 → 0.12.2
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/CHANGELOG.md +20 -0
- package/README.md +11 -9
- package/agent-format.md +4 -2
- package/agents/general-purpose.md +4 -2
- package/agents/planner.md +2 -1
- package/agents/reviewer.md +2 -1
- package/agents/scout.md +3 -3
- package/agents/tester.md +3 -3
- package/agents/worker.md +4 -2
- package/extensions/agents.ts +310 -310
- package/extensions/index.ts +1357 -1357
- package/extensions/model.ts +41 -41
- package/extensions/render.ts +205 -205
- package/extensions/runner.ts +200 -200
- package/extensions/security.ts +313 -313
- package/extensions/service.ts +115 -115
- package/extensions/thread-viewer.ts +246 -246
- package/extensions/threads.ts +99 -99
- package/package.json +9 -9
package/extensions/agents.ts
CHANGED
|
@@ -16,242 +16,242 @@ export type AgentScope = "user" | "project" | "both";
|
|
|
16
16
|
export type AgentColor = "red" | "blue" | "green" | "yellow" | "purple" | "orange" | "pink" | "cyan";
|
|
17
17
|
|
|
18
18
|
export interface AgentConfig {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
tools?: string[];
|
|
22
|
+
model?: string;
|
|
23
|
+
models?: string[];
|
|
24
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
25
|
+
sandbox?: "read-only" | "workspace-write";
|
|
26
|
+
color?: AgentColor;
|
|
27
|
+
systemPrompt: string;
|
|
28
|
+
source: "user" | "project" | "bundled";
|
|
29
|
+
filePath: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function getModelCandidates(agent: Pick<AgentConfig, "model" | "models">): string[] {
|
|
33
|
-
|
|
33
|
+
return [...new Set([agent.model, ...(agent.models ?? [])].filter((model): model is string => Boolean(model)))];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface AgentDiscoveryResult {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
agents: AgentConfig[];
|
|
38
|
+
projectAgentsDir: string | null;
|
|
39
|
+
diagnostics: AgentDiscoveryDiagnostic[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface AgentDiscoveryDiagnostic {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
filePath: string;
|
|
44
|
+
issue: string;
|
|
45
|
+
/** 'warn' for recoverable issues, 'error' for file-skip issues. */
|
|
46
|
+
severity: "warn" | "error";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
interface AgentCache {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
userDir: string;
|
|
51
|
+
projectDir: string | null;
|
|
52
|
+
bundledDir: string;
|
|
53
|
+
scope: AgentScope;
|
|
54
|
+
agents: AgentConfig[];
|
|
55
|
+
projectAgentsDir: string | null;
|
|
56
|
+
diagnostics: AgentDiscoveryDiagnostic[];
|
|
57
|
+
/** File-level signature per directory (name:mtime:size for each .md file) */
|
|
58
|
+
dirSignatures: Map<string, string>;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
let _cache: AgentCache | null = null;
|
|
62
62
|
|
|
63
63
|
/** Clear the agent cache (call on /reload). */
|
|
64
64
|
export function invalidateAgentCache(): void {
|
|
65
|
-
|
|
65
|
+
_cache = null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function loadAgentsFromDir(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
dir: string,
|
|
70
|
+
source: "user" | "project" | "bundled",
|
|
71
|
+
diagnostics: AgentDiscoveryDiagnostic[],
|
|
72
72
|
): AgentConfig[] {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
73
|
+
const agents: AgentConfig[] = [];
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(dir)) return agents;
|
|
76
|
+
|
|
77
|
+
let entries: fs.Dirent[];
|
|
78
|
+
try {
|
|
79
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
diagnostics.push({
|
|
82
|
+
filePath: dir,
|
|
83
|
+
issue: `Cannot read directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
severity: "warn",
|
|
85
|
+
});
|
|
86
|
+
return agents;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
91
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
92
|
+
diagnostics.push({
|
|
93
|
+
filePath: path.join(dir, entry.name),
|
|
94
|
+
issue: `Not a regular file or symlink, skipping.`,
|
|
95
|
+
severity: "warn",
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const filePath = path.join(dir, entry.name);
|
|
101
|
+
let content: string;
|
|
102
|
+
try {
|
|
103
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
filePath,
|
|
107
|
+
issue: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
severity: "error",
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
let frontmatter: Record<string, unknown>;
|
|
116
|
+
let body: string;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
119
|
+
frontmatter = parsed.frontmatter;
|
|
120
|
+
body = parsed.body;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
filePath,
|
|
124
|
+
issue: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
severity: "error",
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
|
|
131
|
+
if (typeof frontmatter.name !== "string" && typeof frontmatter.description !== "string") {
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
filePath,
|
|
134
|
+
issue: `Missing both "name" and "description" in frontmatter. Agent file skipped.`,
|
|
135
|
+
severity: "error",
|
|
136
|
+
});
|
|
137
|
+
} else if (typeof frontmatter.name !== "string") {
|
|
138
|
+
diagnostics.push({
|
|
139
|
+
filePath,
|
|
140
|
+
issue: `Missing "name" in frontmatter. Agent file skipped.`,
|
|
141
|
+
severity: "error",
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
diagnostics.push({
|
|
145
|
+
filePath,
|
|
146
|
+
issue: `Missing "description" in frontmatter. Agent file skipped.`,
|
|
147
|
+
severity: "error",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!frontmatter.name.trim()) {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
filePath,
|
|
156
|
+
issue: `"name" in frontmatter is empty. Agent file skipped.`,
|
|
157
|
+
severity: "error",
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tools =
|
|
163
|
+
typeof frontmatter.tools === "string"
|
|
164
|
+
? frontmatter.tools.split(",").map((t) => t.trim()).filter(Boolean)
|
|
165
|
+
: Array.isArray(frontmatter.tools)
|
|
166
|
+
? (frontmatter.tools as unknown[]).filter((t): t is string => typeof t === "string")
|
|
167
|
+
: undefined;
|
|
168
|
+
const model = typeof frontmatter.model === "string" ? frontmatter.model.trim() || undefined : undefined;
|
|
169
|
+
const models =
|
|
170
|
+
typeof frontmatter.models === "string"
|
|
171
|
+
? frontmatter.models.split(",").map((item) => item.trim()).filter(Boolean)
|
|
172
|
+
: Array.isArray(frontmatter.models)
|
|
173
|
+
? (frontmatter.models as unknown[]).filter((item): item is string => typeof item === "string" && Boolean(item.trim())).map((item) => item.trim())
|
|
174
|
+
: undefined;
|
|
175
|
+
|
|
176
|
+
if (frontmatter.models !== undefined && typeof frontmatter.models !== "string" && !Array.isArray(frontmatter.models)) {
|
|
177
|
+
diagnostics.push({ filePath, issue: `"models" must be a YAML array or comma-separated string. Ignoring.`, severity: "warn" });
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(frontmatter.models)) {
|
|
180
|
+
for (const item of frontmatter.models) {
|
|
181
|
+
if (typeof item === "string" && item.trim()) continue;
|
|
182
|
+
diagnostics.push({ filePath, issue: `"models" entries must be non-empty strings. Ignoring invalid entry.`, severity: "warn" });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const modelName of getModelCandidates({ model, models })) {
|
|
186
|
+
if (modelName.includes("/")) continue;
|
|
187
|
+
diagnostics.push({
|
|
188
|
+
filePath,
|
|
189
|
+
issue: `Model "${modelName}" does not include a provider prefix (e.g., "anthropic/claude-sonnet-4-20250514"). Resolution may fail.`,
|
|
190
|
+
severity: "warn",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (typeof frontmatter.thinking === "string" && frontmatter.thinking) {
|
|
195
|
+
const validLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
196
|
+
if (!validLevels.includes(frontmatter.thinking)) {
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
filePath,
|
|
199
|
+
issue: `Invalid thinking level "${frontmatter.thinking}". Valid values: ${validLevels.join(", ")}. Using default.`,
|
|
200
|
+
severity: "warn",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof frontmatter.sandbox === "string" && frontmatter.sandbox) {
|
|
206
|
+
const validSandboxes = ["read-only", "workspace-write"];
|
|
207
|
+
if (!validSandboxes.includes(frontmatter.sandbox)) {
|
|
208
|
+
diagnostics.push({
|
|
209
|
+
filePath,
|
|
210
|
+
issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Ignoring.`,
|
|
211
|
+
severity: "warn",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const VALID_COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"] as const;
|
|
217
|
+
if (typeof frontmatter.color === "string" && frontmatter.color && !VALID_COLORS.includes(frontmatter.color as any)) {
|
|
218
|
+
diagnostics.push({
|
|
219
|
+
filePath,
|
|
220
|
+
issue: `Invalid color "${frontmatter.color}". Valid values: ${VALID_COLORS.join(", ")}. Ignoring.`,
|
|
221
|
+
severity: "warn",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
agents.push({
|
|
226
|
+
name: frontmatter.name,
|
|
227
|
+
description: frontmatter.description,
|
|
228
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
229
|
+
model,
|
|
230
|
+
models: models && models.length > 0 ? models : undefined,
|
|
231
|
+
thinking: typeof frontmatter.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(frontmatter.thinking)
|
|
232
|
+
? frontmatter.thinking as AgentConfig["thinking"]
|
|
233
|
+
: undefined,
|
|
234
|
+
sandbox: typeof frontmatter.sandbox === "string" && ["read-only", "workspace-write"].includes(frontmatter.sandbox)
|
|
235
|
+
? frontmatter.sandbox as "read-only" | "workspace-write"
|
|
236
|
+
: undefined,
|
|
237
|
+
color: typeof frontmatter.color === "string" && VALID_COLORS.includes(frontmatter.color as any)
|
|
238
|
+
? frontmatter.color as AgentColor
|
|
239
|
+
: undefined,
|
|
240
|
+
systemPrompt: body,
|
|
241
|
+
source,
|
|
242
|
+
filePath,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return agents;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function isDirectory(p: string): boolean {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
try {
|
|
251
|
+
return fs.statSync(p).isDirectory();
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/** Build a stable signature for agent .md files in a directory.
|
|
@@ -259,35 +259,35 @@ function isDirectory(p: string): boolean {
|
|
|
259
259
|
* list of `name:mtimeMs:size` entries that catches both content
|
|
260
260
|
* edits and add/remove/rename operations. */
|
|
261
261
|
function dirSignature(dir: string): string {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
try {
|
|
263
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
264
|
+
.filter((e) => e.name.endsWith(".md") && (e.isFile() || e.isSymbolicLink()))
|
|
265
|
+
.map((e) => {
|
|
266
|
+
const file = path.join(dir, e.name);
|
|
267
|
+
try {
|
|
268
|
+
const st = fs.statSync(file);
|
|
269
|
+
return `${e.name}:${st.mtimeMs}:${st.size}`;
|
|
270
|
+
} catch {
|
|
271
|
+
return `${e.name}:broken`;
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.sort();
|
|
275
|
+
return `exists:${entries.join("|")}`;
|
|
276
|
+
} catch {
|
|
277
|
+
return "missing";
|
|
278
|
+
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
let currentDir = cwd;
|
|
283
|
+
while (true) {
|
|
284
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
285
|
+
if (isDirectory(candidate)) return candidate;
|
|
286
|
+
|
|
287
|
+
const parentDir = path.dirname(currentDir);
|
|
288
|
+
if (parentDir === currentDir) return null;
|
|
289
|
+
currentDir = parentDir;
|
|
290
|
+
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
/**
|
|
@@ -297,81 +297,81 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
297
297
|
* @param bundledAgentsDir - Path to skill-bundled agents directory.
|
|
298
298
|
*/
|
|
299
299
|
export function discoverAgents(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
cwd: string,
|
|
301
|
+
scope: AgentScope,
|
|
302
|
+
bundledAgentsDir: string,
|
|
303
303
|
): AgentDiscoveryResult {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
304
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
305
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
306
|
+
|
|
307
|
+
// Check cache (with file-signature invalidation so editing agent .md files auto-detects changes)
|
|
308
|
+
if (
|
|
309
|
+
_cache &&
|
|
310
|
+
_cache.userDir === userDir &&
|
|
311
|
+
_cache.projectDir === projectAgentsDir &&
|
|
312
|
+
_cache.bundledDir === bundledAgentsDir &&
|
|
313
|
+
_cache.scope === scope
|
|
314
|
+
) {
|
|
315
|
+
let stale = false;
|
|
316
|
+
for (const [dir, cachedSig] of _cache.dirSignatures) {
|
|
317
|
+
if (dirSignature(dir) !== cachedSig) {
|
|
318
|
+
stale = true;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!stale) {
|
|
323
|
+
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics: _cache.diagnostics };
|
|
324
|
+
}
|
|
325
|
+
// Cache is stale — rebuild below
|
|
326
|
+
_cache = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const diagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
330
|
+
|
|
331
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user", diagnostics);
|
|
332
|
+
const projectAgents =
|
|
333
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project", diagnostics);
|
|
334
|
+
const bundledAgents = loadAgentsFromDir(bundledAgentsDir, "bundled", diagnostics);
|
|
335
|
+
|
|
336
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
337
|
+
|
|
338
|
+
// Priority: bundled < user < project (higher index = higher priority)
|
|
339
|
+
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
340
|
+
if (scope === "both" || scope === "user") {
|
|
341
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
342
|
+
}
|
|
343
|
+
if (scope === "both" || scope === "project") {
|
|
344
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const agents = Array.from(agentMap.values());
|
|
348
|
+
|
|
349
|
+
const dirSignatures = new Map<string, string>();
|
|
350
|
+
for (const dir of [userDir, projectAgentsDir, bundledAgentsDir]) {
|
|
351
|
+
if (!dir) continue;
|
|
352
|
+
dirSignatures.set(dir, dirSignature(dir));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_cache = {
|
|
356
|
+
userDir,
|
|
357
|
+
projectDir: projectAgentsDir,
|
|
358
|
+
bundledDir: bundledAgentsDir,
|
|
359
|
+
scope,
|
|
360
|
+
agents,
|
|
361
|
+
projectAgentsDir,
|
|
362
|
+
diagnostics,
|
|
363
|
+
dirSignatures,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
return { agents, projectAgentsDir, diagnostics };
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
370
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
371
|
+
const listed = agents.slice(0, maxItems);
|
|
372
|
+
const remaining = agents.length - listed.length;
|
|
373
|
+
return {
|
|
374
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
375
|
+
remaining,
|
|
376
|
+
};
|
|
377
377
|
}
|