pi-extended-teams 2.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { ThinkingLevel } from "./models";
|
|
5
|
+
import type { AgentDefinition, PredefinedTeam, SaveTeamTemplateOptions, SaveTeamTemplateResult } from "./predefined-teams/types";
|
|
6
|
+
export type { AgentDefinition, PredefinedTeam, SaveTeamTemplateOptions, SaveTeamTemplateResult } from "./predefined-teams/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse frontmatter from a markdown file
|
|
10
|
+
*/
|
|
11
|
+
export function parseAgentFrontmatter(content: string, filePath: string): AgentDefinition | null {
|
|
12
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
13
|
+
if (!frontmatterMatch) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const frontmatterStr = frontmatterMatch[1];
|
|
18
|
+
const prompt = frontmatterMatch[2].trim();
|
|
19
|
+
const frontmatter: Record<string, string> = {};
|
|
20
|
+
|
|
21
|
+
// Parse YAML-like frontmatter (simple key: value format)
|
|
22
|
+
for (const line of frontmatterStr.split("\n")) {
|
|
23
|
+
const colonIndex = line.indexOf(":");
|
|
24
|
+
if (colonIndex > 0) {
|
|
25
|
+
const key = line.slice(0, colonIndex).trim();
|
|
26
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
27
|
+
frontmatter[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const name = frontmatter.name;
|
|
32
|
+
if (!name) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const description = frontmatter.description || "";
|
|
37
|
+
|
|
38
|
+
// Parse tools (comma-separated or space-separated)
|
|
39
|
+
let tools: string[] | undefined;
|
|
40
|
+
if (frontmatter.tools) {
|
|
41
|
+
tools = frontmatter.tools.split(/[,\s]+/).map(t => t.trim()).filter(t => t);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const thinking = frontmatter.thinking as AgentDefinition["thinking"];
|
|
45
|
+
const model = frontmatter.model;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
description,
|
|
50
|
+
tools,
|
|
51
|
+
model,
|
|
52
|
+
thinking,
|
|
53
|
+
prompt,
|
|
54
|
+
filePath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Discover agent definitions from a directory
|
|
60
|
+
*/
|
|
61
|
+
export function discoverAgents(dir: string): AgentDefinition[] {
|
|
62
|
+
const agents: AgentDefinition[] = [];
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
return agents;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = path.join(dir, entry.name);
|
|
72
|
+
|
|
73
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
74
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
75
|
+
const agent = parseAgentFrontmatter(content, fullPath);
|
|
76
|
+
if (agent) {
|
|
77
|
+
agents.push(agent);
|
|
78
|
+
}
|
|
79
|
+
} else if (entry.isDirectory()) {
|
|
80
|
+
// Check for SKILL.md style (agent-name/SKILL.md)
|
|
81
|
+
const skillPath = path.join(fullPath, "SKILL.md");
|
|
82
|
+
if (fs.existsSync(skillPath)) {
|
|
83
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
84
|
+
const agent = parseAgentFrontmatter(content, skillPath);
|
|
85
|
+
if (agent) {
|
|
86
|
+
agents.push(agent);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return agents;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse teams.yaml content into PredefinedTeam array
|
|
97
|
+
*/
|
|
98
|
+
export function parseTeamsYaml(content: string): PredefinedTeam[] {
|
|
99
|
+
const teams: PredefinedTeam[] = [];
|
|
100
|
+
const lines = content.split("\n");
|
|
101
|
+
let currentTeam: PredefinedTeam | null = null;
|
|
102
|
+
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
// Skip empty lines and comments
|
|
105
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for team definition (no leading spaces, ends with colon)
|
|
110
|
+
if (!line.startsWith(" ") && !line.startsWith("\t") && line.endsWith(":")) {
|
|
111
|
+
// Save previous team
|
|
112
|
+
if (currentTeam && currentTeam.agents.length > 0) {
|
|
113
|
+
teams.push(currentTeam);
|
|
114
|
+
}
|
|
115
|
+
currentTeam = {
|
|
116
|
+
name: line.slice(0, -1).trim(),
|
|
117
|
+
agents: [],
|
|
118
|
+
};
|
|
119
|
+
} else if (currentTeam && (line.startsWith(" ") || line.startsWith("\t"))) {
|
|
120
|
+
// Agent entry (indented line starting with -)
|
|
121
|
+
const agentMatch = line.match(/^\s*-\s*(.+)$/);
|
|
122
|
+
if (agentMatch) {
|
|
123
|
+
currentTeam.agents.push(agentMatch[1].trim());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Save last team
|
|
129
|
+
if (currentTeam && currentTeam.agents.length > 0) {
|
|
130
|
+
teams.push(currentTeam);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return teams;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Discover predefined teams from teams.yaml files
|
|
138
|
+
*/
|
|
139
|
+
export function discoverTeams(dir: string): PredefinedTeam[] {
|
|
140
|
+
const teamsPath = path.join(dir, "teams.yaml");
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(teamsPath)) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const content = fs.readFileSync(teamsPath, "utf-8");
|
|
148
|
+
return parseTeamsYaml(content);
|
|
149
|
+
} catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get all agent definitions from all locations
|
|
156
|
+
* Priority: project-local > global
|
|
157
|
+
*/
|
|
158
|
+
export function getAllAgentDefinitions(projectDir?: string): AgentDefinition[] {
|
|
159
|
+
const agents: AgentDefinition[] = [];
|
|
160
|
+
const seenNames = new Set<string>();
|
|
161
|
+
|
|
162
|
+
// Global agent definitions
|
|
163
|
+
const globalDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
164
|
+
for (const agent of discoverAgents(globalDir)) {
|
|
165
|
+
if (!seenNames.has(agent.name)) {
|
|
166
|
+
seenNames.add(agent.name);
|
|
167
|
+
agents.push(agent);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Project-local agent definitions
|
|
172
|
+
if (projectDir) {
|
|
173
|
+
const projectAgentsDir = path.join(projectDir, ".pi", "agents");
|
|
174
|
+
for (const agent of discoverAgents(projectAgentsDir)) {
|
|
175
|
+
if (!seenNames.has(agent.name)) {
|
|
176
|
+
seenNames.add(agent.name);
|
|
177
|
+
agents.push(agent);
|
|
178
|
+
} else {
|
|
179
|
+
// Override global with project-local
|
|
180
|
+
const idx = agents.findIndex(a => a.name === agent.name);
|
|
181
|
+
if (idx >= 0) {
|
|
182
|
+
agents[idx] = agent;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return agents;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all predefined teams from all locations
|
|
193
|
+
* Priority: project-local > global
|
|
194
|
+
*/
|
|
195
|
+
export function getAllPredefinedTeams(projectDir?: string): PredefinedTeam[] {
|
|
196
|
+
const teams: PredefinedTeam[] = [];
|
|
197
|
+
const seenNames = new Set<string>();
|
|
198
|
+
|
|
199
|
+
// Global teams: prefer the documented ~/.pi/teams.yaml location,
|
|
200
|
+
// but still fall back to the legacy ~/.pi/agent/teams.yaml path.
|
|
201
|
+
const globalDirs = [
|
|
202
|
+
path.join(os.homedir(), ".pi"),
|
|
203
|
+
path.join(os.homedir(), ".pi", "agent"),
|
|
204
|
+
];
|
|
205
|
+
for (const globalDir of globalDirs) {
|
|
206
|
+
for (const team of discoverTeams(globalDir)) {
|
|
207
|
+
if (!seenNames.has(team.name)) {
|
|
208
|
+
seenNames.add(team.name);
|
|
209
|
+
teams.push(team);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Project-local teams
|
|
215
|
+
if (projectDir) {
|
|
216
|
+
const projectDirPath = path.join(projectDir, ".pi");
|
|
217
|
+
for (const team of discoverTeams(projectDirPath)) {
|
|
218
|
+
if (!seenNames.has(team.name)) {
|
|
219
|
+
seenNames.add(team.name);
|
|
220
|
+
teams.push(team);
|
|
221
|
+
} else {
|
|
222
|
+
// Override global with project-local
|
|
223
|
+
const idx = teams.findIndex(t => t.name === team.name);
|
|
224
|
+
if (idx >= 0) {
|
|
225
|
+
teams[idx] = team;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return teams;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get a specific agent definition by name
|
|
236
|
+
*/
|
|
237
|
+
export function getAgentDefinition(name: string, projectDir?: string): AgentDefinition | undefined {
|
|
238
|
+
const agents = getAllAgentDefinitions(projectDir);
|
|
239
|
+
return agents.find(a => a.name === name);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get a specific predefined team by name
|
|
244
|
+
*/
|
|
245
|
+
export function getPredefinedTeam(name: string, projectDir?: string): PredefinedTeam | undefined {
|
|
246
|
+
const teams = getAllPredefinedTeams(projectDir);
|
|
247
|
+
return teams.find(t => t.name === name);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate markdown content for an agent definition file
|
|
252
|
+
*/
|
|
253
|
+
export function generateAgentMarkdown(agent: {
|
|
254
|
+
name: string;
|
|
255
|
+
description?: string;
|
|
256
|
+
tools?: string[];
|
|
257
|
+
model?: string;
|
|
258
|
+
thinking?: ThinkingLevel;
|
|
259
|
+
prompt?: string;
|
|
260
|
+
}): string {
|
|
261
|
+
const lines: string[] = ["---"];
|
|
262
|
+
lines.push(`name: ${agent.name}`);
|
|
263
|
+
if (agent.description) {
|
|
264
|
+
lines.push(`description: ${agent.description}`);
|
|
265
|
+
}
|
|
266
|
+
if (agent.tools && agent.tools.length > 0) {
|
|
267
|
+
lines.push(`tools: ${agent.tools.join(", ")}`);
|
|
268
|
+
}
|
|
269
|
+
if (agent.model) {
|
|
270
|
+
lines.push(`model: ${agent.model}`);
|
|
271
|
+
}
|
|
272
|
+
if (agent.thinking) {
|
|
273
|
+
lines.push(`thinking: ${agent.thinking}`);
|
|
274
|
+
}
|
|
275
|
+
lines.push("---");
|
|
276
|
+
lines.push("");
|
|
277
|
+
if (agent.prompt) {
|
|
278
|
+
lines.push(agent.prompt);
|
|
279
|
+
}
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate teams.yaml content by adding a new team template
|
|
285
|
+
*/
|
|
286
|
+
export function generateTeamsYamlWithTemplate(
|
|
287
|
+
existingContent: string,
|
|
288
|
+
templateName: string,
|
|
289
|
+
agentNames: string[],
|
|
290
|
+
description?: string
|
|
291
|
+
): string {
|
|
292
|
+
// Check if template already exists
|
|
293
|
+
const lines = existingContent.split("\n");
|
|
294
|
+
let templateExists = false;
|
|
295
|
+
let templateStartLine = -1;
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < lines.length; i++) {
|
|
298
|
+
if (lines[i].trim() === `${templateName}:`) {
|
|
299
|
+
templateExists = true;
|
|
300
|
+
templateStartLine = i;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (templateExists) {
|
|
306
|
+
// Replace existing template - find where it ends
|
|
307
|
+
let templateEndLine = templateStartLine + 1;
|
|
308
|
+
while (templateEndLine < lines.length && (lines[templateEndLine].startsWith(" ") || lines[templateEndLine].startsWith("\t"))) {
|
|
309
|
+
templateEndLine++;
|
|
310
|
+
}
|
|
311
|
+
// Remove old template lines
|
|
312
|
+
lines.splice(templateStartLine, templateEndLine - templateStartLine);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Build new template entry
|
|
316
|
+
const templateLines: string[] = [];
|
|
317
|
+
if (description) {
|
|
318
|
+
templateLines.push(`# ${description}`);
|
|
319
|
+
}
|
|
320
|
+
templateLines.push(`${templateName}:`);
|
|
321
|
+
for (const agentName of agentNames) {
|
|
322
|
+
templateLines.push(` - ${agentName}`);
|
|
323
|
+
}
|
|
324
|
+
templateLines.push("");
|
|
325
|
+
|
|
326
|
+
// Find insertion point (at the end or after existing content)
|
|
327
|
+
let insertIndex = lines.length;
|
|
328
|
+
|
|
329
|
+
// Remove trailing empty lines to find actual end
|
|
330
|
+
while (insertIndex > 0 && lines[insertIndex - 1].trim() === "") {
|
|
331
|
+
insertIndex--;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Insert new template
|
|
335
|
+
lines.splice(insertIndex, 0, ...templateLines);
|
|
336
|
+
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Save a team configuration as a reusable template.
|
|
342
|
+
* Creates agent definition files and updates teams.yaml.
|
|
343
|
+
*/
|
|
344
|
+
export function saveTeamTemplate(
|
|
345
|
+
teamConfig: {
|
|
346
|
+
name: string;
|
|
347
|
+
description?: string;
|
|
348
|
+
members: Array<{
|
|
349
|
+
name: string;
|
|
350
|
+
agentType: string;
|
|
351
|
+
model?: string;
|
|
352
|
+
thinking?: ThinkingLevel;
|
|
353
|
+
prompt?: string;
|
|
354
|
+
}>;
|
|
355
|
+
defaultModel?: string;
|
|
356
|
+
},
|
|
357
|
+
options: SaveTeamTemplateOptions
|
|
358
|
+
): SaveTeamTemplateResult {
|
|
359
|
+
// Determine output paths based on scope
|
|
360
|
+
const agentsDir = options.scope === "project"
|
|
361
|
+
? path.join(options.projectDir || process.cwd(), ".pi", "agents")
|
|
362
|
+
: path.join(os.homedir(), ".pi", "agent", "agents");
|
|
363
|
+
|
|
364
|
+
const teamsYamlPath = options.scope === "project"
|
|
365
|
+
? path.join(options.projectDir || process.cwd(), ".pi", "teams.yaml")
|
|
366
|
+
: path.join(os.homedir(), ".pi", "teams.yaml");
|
|
367
|
+
|
|
368
|
+
// Ensure agents directory exists
|
|
369
|
+
if (!fs.existsSync(agentsDir)) {
|
|
370
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Filter to only teammates (not the lead)
|
|
374
|
+
const teammates = teamConfig.members.filter(m => m.agentType === "teammate");
|
|
375
|
+
const agentNames: string[] = [];
|
|
376
|
+
const savedAgents: SaveTeamTemplateResult["savedAgents"] = [];
|
|
377
|
+
|
|
378
|
+
// Save each teammate as an agent definition
|
|
379
|
+
for (const member of teammates) {
|
|
380
|
+
const agentFileName = `${member.name}.md`;
|
|
381
|
+
const agentPath = path.join(agentsDir, agentFileName);
|
|
382
|
+
const existed = fs.existsSync(agentPath);
|
|
383
|
+
|
|
384
|
+
// Use the model from the member, or fall back to the team's default model
|
|
385
|
+
const model = member.model || teamConfig.defaultModel;
|
|
386
|
+
|
|
387
|
+
const content = generateAgentMarkdown({
|
|
388
|
+
name: member.name,
|
|
389
|
+
description: `Agent from team '${teamConfig.name}'`,
|
|
390
|
+
model,
|
|
391
|
+
thinking: member.thinking,
|
|
392
|
+
prompt: member.prompt,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
fs.writeFileSync(agentPath, content);
|
|
396
|
+
agentNames.push(member.name);
|
|
397
|
+
savedAgents.push({ name: member.name, path: agentPath, existed });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Update teams.yaml
|
|
401
|
+
let teamsContent = "";
|
|
402
|
+
if (fs.existsSync(teamsYamlPath)) {
|
|
403
|
+
teamsContent = fs.readFileSync(teamsYamlPath, "utf-8");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Check if template already exists
|
|
407
|
+
const templateExisted = teamsContent.includes(`${options.templateName}:`);
|
|
408
|
+
|
|
409
|
+
// Generate updated teams.yaml content
|
|
410
|
+
const updatedContent = generateTeamsYamlWithTemplate(
|
|
411
|
+
teamsContent,
|
|
412
|
+
options.templateName,
|
|
413
|
+
agentNames,
|
|
414
|
+
options.description || teamConfig.description
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
fs.writeFileSync(teamsYamlPath, updatedContent);
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
templateName: options.templateName,
|
|
421
|
+
agentsDir,
|
|
422
|
+
teamsYamlPath,
|
|
423
|
+
savedAgents,
|
|
424
|
+
templateExisted,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* List all runtime team configurations from ~/.pi/teams/
|
|
430
|
+
*/
|
|
431
|
+
export function listRuntimeTeams(): Array<{
|
|
432
|
+
name: string;
|
|
433
|
+
description?: string;
|
|
434
|
+
memberCount: number;
|
|
435
|
+
createdAt?: number;
|
|
436
|
+
}> {
|
|
437
|
+
const teamsDir = path.join(os.homedir(), ".pi", "teams");
|
|
438
|
+
|
|
439
|
+
if (!fs.existsSync(teamsDir)) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const teams: Array<{
|
|
444
|
+
name: string;
|
|
445
|
+
description?: string;
|
|
446
|
+
memberCount: number;
|
|
447
|
+
createdAt?: number;
|
|
448
|
+
}> = [];
|
|
449
|
+
|
|
450
|
+
for (const teamDir of fs.readdirSync(teamsDir, { withFileTypes: true })) {
|
|
451
|
+
if (!teamDir.isDirectory()) continue;
|
|
452
|
+
|
|
453
|
+
const configPath = path.join(teamsDir, teamDir.name, "config.json");
|
|
454
|
+
|
|
455
|
+
if (fs.existsSync(configPath)) {
|
|
456
|
+
try {
|
|
457
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
458
|
+
teams.push({
|
|
459
|
+
name: config.name || teamDir.name,
|
|
460
|
+
description: config.description,
|
|
461
|
+
memberCount: (config.members || []).filter((m: any) => m.agentType === "teammate").length,
|
|
462
|
+
createdAt: config.createdAt,
|
|
463
|
+
});
|
|
464
|
+
} catch {
|
|
465
|
+
// Skip invalid config files
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return teams;
|
|
471
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJsonAtomic } from "./atomic-json";
|
|
5
|
+
import { withLock } from "./lock";
|
|
6
|
+
import { readHelperQueuePath } from "./paths";
|
|
7
|
+
import type { FavoriteModelSlot } from "./settings";
|
|
8
|
+
|
|
9
|
+
export interface QueuedReadHelperRequest {
|
|
10
|
+
id: string;
|
|
11
|
+
teamName: string;
|
|
12
|
+
requester: string;
|
|
13
|
+
name: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
modelSlot: FavoriteModelSlot;
|
|
17
|
+
requestedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureQueuePath(teamName: string): string {
|
|
21
|
+
const queuePath = readHelperQueuePath(teamName);
|
|
22
|
+
const dir = path.dirname(queuePath);
|
|
23
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
return queuePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readQueueRaw(queuePath: string): QueuedReadHelperRequest[] {
|
|
28
|
+
if (!fs.existsSync(queuePath)) return [];
|
|
29
|
+
const raw = JSON.parse(fs.readFileSync(queuePath, "utf-8"));
|
|
30
|
+
return Array.isArray(raw) ? raw : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeQueueRaw(queuePath: string, queue: QueuedReadHelperRequest[]): void {
|
|
34
|
+
writeJsonAtomic(queuePath, queue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function listReadHelperQueue(teamName: string): Promise<QueuedReadHelperRequest[]> {
|
|
38
|
+
const queuePath = ensureQueuePath(teamName);
|
|
39
|
+
return await withLock(queuePath, async () => readQueueRaw(queuePath));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function enqueueReadHelperRequest(
|
|
43
|
+
teamName: string,
|
|
44
|
+
request: Omit<QueuedReadHelperRequest, "id" | "teamName" | "requestedAt"> & Partial<Pick<QueuedReadHelperRequest, "id" | "requestedAt">>
|
|
45
|
+
): Promise<QueuedReadHelperRequest> {
|
|
46
|
+
const queuePath = ensureQueuePath(teamName);
|
|
47
|
+
return await withLock(queuePath, async () => {
|
|
48
|
+
const queue = readQueueRaw(queuePath);
|
|
49
|
+
if (queue.some(item => item.name === request.name)) {
|
|
50
|
+
throw new Error(`Read helper request ${request.name} is already queued for team ${teamName}. Choose a different helper name.`);
|
|
51
|
+
}
|
|
52
|
+
const queued: QueuedReadHelperRequest = {
|
|
53
|
+
id: request.id || crypto.randomUUID(),
|
|
54
|
+
teamName,
|
|
55
|
+
requester: request.requester,
|
|
56
|
+
name: request.name,
|
|
57
|
+
prompt: request.prompt,
|
|
58
|
+
cwd: request.cwd,
|
|
59
|
+
modelSlot: request.modelSlot,
|
|
60
|
+
requestedAt: request.requestedAt || Date.now(),
|
|
61
|
+
};
|
|
62
|
+
queue.push(queued);
|
|
63
|
+
writeQueueRaw(queuePath, queue);
|
|
64
|
+
return queued;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function dequeueReadHelperRequest(teamName: string): Promise<QueuedReadHelperRequest | null> {
|
|
69
|
+
const queuePath = ensureQueuePath(teamName);
|
|
70
|
+
return await withLock(queuePath, async () => {
|
|
71
|
+
const queue = readQueueRaw(queuePath);
|
|
72
|
+
const next = queue.shift() || null;
|
|
73
|
+
writeQueueRaw(queuePath, queue);
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function removeQueuedReadHelperRequest(teamName: string, id: string): Promise<QueuedReadHelperRequest | null> {
|
|
79
|
+
const queuePath = ensureQueuePath(teamName);
|
|
80
|
+
return await withLock(queuePath, async () => {
|
|
81
|
+
const queue = readQueueRaw(queuePath);
|
|
82
|
+
const index = queue.findIndex(item => item.id === id);
|
|
83
|
+
if (index === -1) return null;
|
|
84
|
+
const [removed] = queue.splice(index, 1);
|
|
85
|
+
writeQueueRaw(queuePath, queue);
|
|
86
|
+
return removed;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function removeQueuedReadHelpersByName(teamName: string, name: string): Promise<QueuedReadHelperRequest[]> {
|
|
91
|
+
const queuePath = ensureQueuePath(teamName);
|
|
92
|
+
return await withLock(queuePath, async () => {
|
|
93
|
+
const queue = readQueueRaw(queuePath);
|
|
94
|
+
const removed = queue.filter(item => item.name === name);
|
|
95
|
+
const kept = queue.filter(item => item.name !== name);
|
|
96
|
+
if (removed.length > 0) writeQueueRaw(queuePath, kept);
|
|
97
|
+
return removed;
|
|
98
|
+
});
|
|
99
|
+
}
|