pi-teams 0.9.6 → 0.9.8
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 +53 -0
- package/extensions/index.ts +169 -1
- package/package.json +1 -1
- package/src/utils/predefined-teams.ts +248 -0
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ pi install npm:pi-teams
|
|
|
46
46
|
|
|
47
47
|
### Advanced Features
|
|
48
48
|
- **Predefined Teams**: Define team templates in `teams.yaml` and spawn entire teams with a single command.
|
|
49
|
+
- **Save Teams as Templates**: Convert any runtime team into a reusable template with a single command.
|
|
49
50
|
- **Isolated OS Windows**: Launch teammates in true separate OS windows instead of panes.
|
|
50
51
|
- **Persistent Window Titles**: Windows are automatically titled `[team-name]: [agent-name]` for easy identification in your window manager.
|
|
51
52
|
- **Plan Approval Mode**: Require teammates to submit their implementation plans for your approval before they touch any code.
|
|
@@ -107,6 +108,13 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
|
|
107
108
|
### 5. Shut Down Team
|
|
108
109
|
> **You:** "We're done. Shut down the team and close the panes."
|
|
109
110
|
|
|
111
|
+
**Automatic Cleanup:**
|
|
112
|
+
When you shut down a team, pi-teams automatically cleans up orphaned agent session folders from `~/.pi/agent/teams/` that are older than 1 hour. This prevents accumulation of stale session data over time.
|
|
113
|
+
|
|
114
|
+
**Manual Cleanup:**
|
|
115
|
+
If you need to clean up agent sessions without shutting down a team, or want to use a different age threshold:
|
|
116
|
+
> **You:** "Clean up agent session folders older than 24 hours."
|
|
117
|
+
|
|
110
118
|
---
|
|
111
119
|
|
|
112
120
|
## 🏗️ Predefined Teams
|
|
@@ -198,6 +206,51 @@ This single command:
|
|
|
198
206
|
|
|
199
207
|
---
|
|
200
208
|
|
|
209
|
+
## 💾 Save Teams as Templates
|
|
210
|
+
|
|
211
|
+
Sometimes you create a team with custom prompts and settings that you'd like to reuse later. Instead of manually creating `teams.yaml` and agent definition files, you can save any runtime team as a template.
|
|
212
|
+
|
|
213
|
+
### The Workflow
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
CREATE → USE → SAVE → REUSE
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
1. **Create** a team with custom teammates and prompts
|
|
220
|
+
2. **Use** the team for your task
|
|
221
|
+
3. **Save** the team as a reusable template
|
|
222
|
+
4. **Reuse** the template later (even on different projects)
|
|
223
|
+
|
|
224
|
+
### List Runtime Teams
|
|
225
|
+
|
|
226
|
+
See which teams you have that can be saved:
|
|
227
|
+
|
|
228
|
+
> **You:** "List all runtime teams."
|
|
229
|
+
|
|
230
|
+
### Save a Team as a Template
|
|
231
|
+
|
|
232
|
+
> **You:** "Save team 'my-modularization-team' as template 'code-modularization'"
|
|
233
|
+
|
|
234
|
+
This creates:
|
|
235
|
+
- Agent definition files in `~/.pi/agent/agents/` for each teammate
|
|
236
|
+
- Updates `~/.pi/teams.yaml` with the new template
|
|
237
|
+
|
|
238
|
+
### Save to Project-Local Scope
|
|
239
|
+
|
|
240
|
+
To save a template that's specific to the current project:
|
|
241
|
+
|
|
242
|
+
> **You:** "Save team 'my-frontend-team' as template 'frontend-sprint' with scope 'project'"
|
|
243
|
+
|
|
244
|
+
This creates files in `.pi/agents/` and `.pi/teams.yaml` in the current project directory.
|
|
245
|
+
|
|
246
|
+
### Reuse Your Template
|
|
247
|
+
|
|
248
|
+
Once saved, use it just like any predefined team:
|
|
249
|
+
|
|
250
|
+
> **You:** "Create a team named 'auth-refactor' from the 'code-modularization' template in the current directory"
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
201
254
|
## 📚 Learn More
|
|
202
255
|
|
|
203
256
|
- **[Full Usage Guide](docs/guide.md)** - Detailed examples, hook system, best practices, and troubleshooting
|
package/extensions/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
|
|
|
12
12
|
import * as predefined from "../src/utils/predefined-teams";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
15
16
|
import { spawnSync } from "node:child_process";
|
|
16
17
|
|
|
17
18
|
// Cache for available models
|
|
@@ -263,6 +264,45 @@ function cleanupStaleTeam(teamName: string, terminal: any): boolean {
|
|
|
263
264
|
return false;
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Clean up orphaned agent session folders from ~/.pi/agent/teams/
|
|
269
|
+
* These are created by the pi core system when agents are spawned.
|
|
270
|
+
* We remove folders that are older than 24 hours to avoid deleting active sessions.
|
|
271
|
+
* Returns the number of folders cleaned up.
|
|
272
|
+
*/
|
|
273
|
+
function cleanupAgentSessionFolders(maxAgeMs: number = 24 * 60 * 60 * 1000): number {
|
|
274
|
+
const agentTeamsDir = path.join(os.homedir(), ".pi", "agent", "teams");
|
|
275
|
+
if (!fs.existsSync(agentTeamsDir)) return 0;
|
|
276
|
+
|
|
277
|
+
let cleaned = 0;
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
|
|
280
|
+
for (const dir of fs.readdirSync(agentTeamsDir)) {
|
|
281
|
+
const sessionDir = path.join(agentTeamsDir, dir);
|
|
282
|
+
const configFile = path.join(sessionDir, "config.json");
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// Check if this is a directory with a config.json
|
|
286
|
+
if (!fs.statSync(sessionDir).isDirectory()) continue;
|
|
287
|
+
if (!fs.existsSync(configFile)) continue;
|
|
288
|
+
|
|
289
|
+
// Read the config to check the creation time
|
|
290
|
+
const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
291
|
+
const createdAt = config.createdAt ? new Date(config.createdAt).getTime() : 0;
|
|
292
|
+
|
|
293
|
+
// If the folder is older than maxAgeMs, delete it
|
|
294
|
+
if (createdAt > 0 && (now - createdAt) > maxAgeMs) {
|
|
295
|
+
fs.rmSync(sessionDir, { recursive: true });
|
|
296
|
+
cleaned++;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore errors for individual folders
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return cleaned;
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
export default function (pi: ExtensionAPI) {
|
|
267
307
|
const isTeammate = !!process.env.PI_AGENT_NAME;
|
|
268
308
|
const agentName = process.env.PI_AGENT_NAME || "team-lead";
|
|
@@ -794,13 +834,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
794
834
|
const tasksDir = paths.taskDir(teamName);
|
|
795
835
|
if (fs.existsSync(tasksDir)) fs.rmSync(tasksDir, { recursive: true });
|
|
796
836
|
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
797
|
-
|
|
837
|
+
|
|
838
|
+
// Clean up orphaned agent session folders (older than 1 hour)
|
|
839
|
+
const cleanedSessions = cleanupAgentSessionFolders(60 * 60 * 1000);
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
content: [{
|
|
843
|
+
type: "text",
|
|
844
|
+
text: `Team ${teamName} shut down.${cleanedSessions > 0 ? ` Cleaned up ${cleanedSessions} orphaned agent session folder(s).` : ""}`
|
|
845
|
+
}],
|
|
846
|
+
details: { cleanedSessions }
|
|
847
|
+
};
|
|
798
848
|
} catch (e) {
|
|
799
849
|
throw new Error(`Failed to shutdown team: ${e}`);
|
|
800
850
|
}
|
|
801
851
|
},
|
|
802
852
|
});
|
|
803
853
|
|
|
854
|
+
pi.registerTool({
|
|
855
|
+
name: "cleanup_agent_sessions",
|
|
856
|
+
label: "Cleanup Agent Sessions",
|
|
857
|
+
description: "Clean up orphaned agent session folders from ~/.pi/agent/teams/ that are older than a specified age.",
|
|
858
|
+
parameters: Type.Object({
|
|
859
|
+
max_age_hours: Type.Optional(Type.Number()),
|
|
860
|
+
}),
|
|
861
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
862
|
+
const maxAgeHours = params.max_age_hours ?? 24;
|
|
863
|
+
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
864
|
+
const cleaned = cleanupAgentSessionFolders(maxAgeMs);
|
|
865
|
+
return {
|
|
866
|
+
content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: `Cleaned up ${cleaned} orphaned agent session folder(s) older than ${maxAgeHours} hour(s).`
|
|
869
|
+
}],
|
|
870
|
+
details: { cleaned, maxAgeHours }
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
|
|
804
875
|
pi.registerTool({
|
|
805
876
|
name: "task_read",
|
|
806
877
|
label: "Read Task",
|
|
@@ -1110,4 +1181,101 @@ export default function (pi: ExtensionAPI) {
|
|
|
1110
1181
|
};
|
|
1111
1182
|
},
|
|
1112
1183
|
});
|
|
1184
|
+
|
|
1185
|
+
pi.registerTool({
|
|
1186
|
+
name: "save_team_as_template",
|
|
1187
|
+
label: "Save Team as Template",
|
|
1188
|
+
description: "Save a runtime team as a reusable predefined team template. Creates agent definition files and updates teams.yaml. Use this when you've created a team with custom prompts and want to reuse it later.",
|
|
1189
|
+
parameters: Type.Object({
|
|
1190
|
+
team_name: Type.String({ description: "Name of the runtime team to save" }),
|
|
1191
|
+
template_name: Type.String({ description: "Name for the template (e.g., 'modularization', 'frontend-team')" }),
|
|
1192
|
+
description: Type.Optional(Type.String({ description: "Description for the template" })),
|
|
1193
|
+
scope: Type.Optional(StringEnum(["user", "project"], { description: "Where to save: 'user' for global (~/.pi), 'project' for project-local (.pi). Defaults to 'user'." })),
|
|
1194
|
+
}),
|
|
1195
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1196
|
+
const teamName = params.team_name;
|
|
1197
|
+
|
|
1198
|
+
// Verify the team exists
|
|
1199
|
+
if (!teams.teamExists(teamName)) {
|
|
1200
|
+
throw new Error(`Team "${teamName}" does not exist. Use list_runtime_teams to see available teams.`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Read the team configuration
|
|
1204
|
+
const config = await teams.readConfig(teamName);
|
|
1205
|
+
|
|
1206
|
+
// Check that there are teammates to save
|
|
1207
|
+
const teammates = config.members.filter(m => m.agentType === "teammate");
|
|
1208
|
+
if (teammates.length === 0) {
|
|
1209
|
+
throw new Error(`Team "${teamName}" has no teammates to save. Only teams with spawned teammates can be saved as templates.`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Save the team as a template
|
|
1213
|
+
const result = predefined.saveTeamTemplate(config, {
|
|
1214
|
+
templateName: params.template_name,
|
|
1215
|
+
description: params.description,
|
|
1216
|
+
scope: params.scope || "user",
|
|
1217
|
+
projectDir: ctx.cwd,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Build summary message
|
|
1221
|
+
const agentSummary = result.savedAgents.map(a =>
|
|
1222
|
+
` - ${a.name}: ${a.existed ? "updated" : "created"} at ${a.path}`
|
|
1223
|
+
).join("\n");
|
|
1224
|
+
|
|
1225
|
+
const message = `Team "${teamName}" saved as template "${params.template_name}".
|
|
1226
|
+
|
|
1227
|
+
Agents saved:
|
|
1228
|
+
${agentSummary}
|
|
1229
|
+
|
|
1230
|
+
Template location: ${result.teamsYamlPath}
|
|
1231
|
+
|
|
1232
|
+
You can now use this template with:
|
|
1233
|
+
create_predefined_team({ team_name: "new-team", predefined_team: "${params.template_name}", cwd: "..." })`;
|
|
1234
|
+
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{ type: "text", text: message }],
|
|
1237
|
+
details: {
|
|
1238
|
+
teamName,
|
|
1239
|
+
templateName: params.template_name,
|
|
1240
|
+
agentsDir: result.agentsDir,
|
|
1241
|
+
teamsYamlPath: result.teamsYamlPath,
|
|
1242
|
+
savedAgents: result.savedAgents,
|
|
1243
|
+
templateExisted: result.templateExisted,
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
pi.registerTool({
|
|
1250
|
+
name: "list_runtime_teams",
|
|
1251
|
+
label: "List Runtime Teams",
|
|
1252
|
+
description: "List all runtime team configurations that can be saved as templates. These are active or saved teams from ~/.pi/teams/.",
|
|
1253
|
+
parameters: Type.Object({}),
|
|
1254
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
1255
|
+
const runtimeTeams = predefined.listRuntimeTeams();
|
|
1256
|
+
|
|
1257
|
+
if (runtimeTeams.length === 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: "No runtime teams found. Create a team with team_create first." }],
|
|
1260
|
+
details: { teams: [] },
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const result = runtimeTeams.map(team => ({
|
|
1265
|
+
name: team.name,
|
|
1266
|
+
description: team.description,
|
|
1267
|
+
memberCount: team.memberCount,
|
|
1268
|
+
createdAt: team.createdAt ? new Date(team.createdAt).toISOString() : undefined,
|
|
1269
|
+
}));
|
|
1270
|
+
|
|
1271
|
+
const summary = result.map(t =>
|
|
1272
|
+
`- ${t.name}: ${t.memberCount} teammate(s)${t.description ? ` - ${t.description}` : ""}`
|
|
1273
|
+
).join("\n");
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
content: [{ type: "text", text: `Runtime teams:\n${summary}` }],
|
|
1277
|
+
details: { teams: result },
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1113
1281
|
}
|
package/package.json
CHANGED
|
@@ -258,4 +258,252 @@ export function getAgentDefinition(name: string, projectDir?: string): AgentDefi
|
|
|
258
258
|
export function getPredefinedTeam(name: string, projectDir?: string): PredefinedTeam | undefined {
|
|
259
259
|
const teams = getAllPredefinedTeams(projectDir);
|
|
260
260
|
return teams.find(t => t.name === name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Options for saving a team as a template
|
|
265
|
+
*/
|
|
266
|
+
export interface SaveTeamTemplateOptions {
|
|
267
|
+
templateName: string;
|
|
268
|
+
description?: string;
|
|
269
|
+
scope: "user" | "project";
|
|
270
|
+
projectDir?: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Result of saving a team as a template
|
|
275
|
+
*/
|
|
276
|
+
export interface SaveTeamTemplateResult {
|
|
277
|
+
templateName: string;
|
|
278
|
+
agentsDir: string;
|
|
279
|
+
teamsYamlPath: string;
|
|
280
|
+
savedAgents: Array<{
|
|
281
|
+
name: string;
|
|
282
|
+
path: string;
|
|
283
|
+
existed: boolean;
|
|
284
|
+
}>;
|
|
285
|
+
templateExisted: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Generate markdown content for an agent definition file
|
|
290
|
+
*/
|
|
291
|
+
export function generateAgentMarkdown(agent: {
|
|
292
|
+
name: string;
|
|
293
|
+
description?: string;
|
|
294
|
+
tools?: string[];
|
|
295
|
+
model?: string;
|
|
296
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high";
|
|
297
|
+
prompt?: string;
|
|
298
|
+
}): string {
|
|
299
|
+
const lines: string[] = ["---"];
|
|
300
|
+
lines.push(`name: ${agent.name}`);
|
|
301
|
+
if (agent.description) {
|
|
302
|
+
lines.push(`description: ${agent.description}`);
|
|
303
|
+
}
|
|
304
|
+
if (agent.tools && agent.tools.length > 0) {
|
|
305
|
+
lines.push(`tools: ${agent.tools.join(", ")}`);
|
|
306
|
+
}
|
|
307
|
+
if (agent.model) {
|
|
308
|
+
lines.push(`model: ${agent.model}`);
|
|
309
|
+
}
|
|
310
|
+
if (agent.thinking) {
|
|
311
|
+
lines.push(`thinking: ${agent.thinking}`);
|
|
312
|
+
}
|
|
313
|
+
lines.push("---");
|
|
314
|
+
lines.push("");
|
|
315
|
+
if (agent.prompt) {
|
|
316
|
+
lines.push(agent.prompt);
|
|
317
|
+
}
|
|
318
|
+
return lines.join("\n");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Generate teams.yaml content by adding a new team template
|
|
323
|
+
*/
|
|
324
|
+
export function generateTeamsYamlWithTemplate(
|
|
325
|
+
existingContent: string,
|
|
326
|
+
templateName: string,
|
|
327
|
+
agentNames: string[],
|
|
328
|
+
description?: string
|
|
329
|
+
): string {
|
|
330
|
+
// Check if template already exists
|
|
331
|
+
const lines = existingContent.split("\n");
|
|
332
|
+
let templateExists = false;
|
|
333
|
+
let templateStartLine = -1;
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
if (lines[i].trim() === `${templateName}:`) {
|
|
337
|
+
templateExists = true;
|
|
338
|
+
templateStartLine = i;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (templateExists) {
|
|
344
|
+
// Replace existing template - find where it ends
|
|
345
|
+
let templateEndLine = templateStartLine + 1;
|
|
346
|
+
while (templateEndLine < lines.length && (lines[templateEndLine].startsWith(" ") || lines[templateEndLine].startsWith("\t"))) {
|
|
347
|
+
templateEndLine++;
|
|
348
|
+
}
|
|
349
|
+
// Remove old template lines
|
|
350
|
+
lines.splice(templateStartLine, templateEndLine - templateStartLine);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Build new template entry
|
|
354
|
+
const templateLines: string[] = [];
|
|
355
|
+
if (description) {
|
|
356
|
+
templateLines.push(`# ${description}`);
|
|
357
|
+
}
|
|
358
|
+
templateLines.push(`${templateName}:`);
|
|
359
|
+
for (const agentName of agentNames) {
|
|
360
|
+
templateLines.push(` - ${agentName}`);
|
|
361
|
+
}
|
|
362
|
+
templateLines.push("");
|
|
363
|
+
|
|
364
|
+
// Find insertion point (at the end or after existing content)
|
|
365
|
+
let insertIndex = lines.length;
|
|
366
|
+
|
|
367
|
+
// Remove trailing empty lines to find actual end
|
|
368
|
+
while (insertIndex > 0 && lines[insertIndex - 1].trim() === "") {
|
|
369
|
+
insertIndex--;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Insert new template
|
|
373
|
+
lines.splice(insertIndex, 0, ...templateLines);
|
|
374
|
+
|
|
375
|
+
return lines.join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Save a team configuration as a reusable template.
|
|
380
|
+
* Creates agent definition files and updates teams.yaml.
|
|
381
|
+
*/
|
|
382
|
+
export function saveTeamTemplate(
|
|
383
|
+
teamConfig: {
|
|
384
|
+
name: string;
|
|
385
|
+
description?: string;
|
|
386
|
+
members: Array<{
|
|
387
|
+
name: string;
|
|
388
|
+
agentType: string;
|
|
389
|
+
model?: string;
|
|
390
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high";
|
|
391
|
+
prompt?: string;
|
|
392
|
+
}>;
|
|
393
|
+
defaultModel?: string;
|
|
394
|
+
},
|
|
395
|
+
options: SaveTeamTemplateOptions
|
|
396
|
+
): SaveTeamTemplateResult {
|
|
397
|
+
// Determine output paths based on scope
|
|
398
|
+
const baseDir = options.scope === "project"
|
|
399
|
+
? path.join(options.projectDir || process.cwd(), ".pi")
|
|
400
|
+
: path.join(os.homedir(), ".pi", "agent");
|
|
401
|
+
|
|
402
|
+
const agentsDir = path.join(baseDir, "agents");
|
|
403
|
+
const teamsYamlPath = path.join(baseDir, "teams.yaml");
|
|
404
|
+
|
|
405
|
+
// Ensure agents directory exists
|
|
406
|
+
if (!fs.existsSync(agentsDir)) {
|
|
407
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Filter to only teammates (not the lead)
|
|
411
|
+
const teammates = teamConfig.members.filter(m => m.agentType === "teammate");
|
|
412
|
+
const agentNames: string[] = [];
|
|
413
|
+
const savedAgents: SaveTeamTemplateResult["savedAgents"] = [];
|
|
414
|
+
|
|
415
|
+
// Save each teammate as an agent definition
|
|
416
|
+
for (const member of teammates) {
|
|
417
|
+
const agentFileName = `${member.name}.md`;
|
|
418
|
+
const agentPath = path.join(agentsDir, agentFileName);
|
|
419
|
+
const existed = fs.existsSync(agentPath);
|
|
420
|
+
|
|
421
|
+
// Use the model from the member, or fall back to the team's default model
|
|
422
|
+
const model = member.model || teamConfig.defaultModel;
|
|
423
|
+
|
|
424
|
+
const content = generateAgentMarkdown({
|
|
425
|
+
name: member.name,
|
|
426
|
+
description: `Agent from team '${teamConfig.name}'`,
|
|
427
|
+
model,
|
|
428
|
+
thinking: member.thinking,
|
|
429
|
+
prompt: member.prompt,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
fs.writeFileSync(agentPath, content);
|
|
433
|
+
agentNames.push(member.name);
|
|
434
|
+
savedAgents.push({ name: member.name, path: agentPath, existed });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Update teams.yaml
|
|
438
|
+
let teamsContent = "";
|
|
439
|
+
if (fs.existsSync(teamsYamlPath)) {
|
|
440
|
+
teamsContent = fs.readFileSync(teamsYamlPath, "utf-8");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check if template already exists
|
|
444
|
+
const templateExisted = teamsContent.includes(`${options.templateName}:`);
|
|
445
|
+
|
|
446
|
+
// Generate updated teams.yaml content
|
|
447
|
+
const updatedContent = generateTeamsYamlWithTemplate(
|
|
448
|
+
teamsContent,
|
|
449
|
+
options.templateName,
|
|
450
|
+
agentNames,
|
|
451
|
+
options.description || teamConfig.description
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
fs.writeFileSync(teamsYamlPath, updatedContent);
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
templateName: options.templateName,
|
|
458
|
+
agentsDir,
|
|
459
|
+
teamsYamlPath,
|
|
460
|
+
savedAgents,
|
|
461
|
+
templateExisted,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* List all runtime team configurations from ~/.pi/teams/
|
|
467
|
+
*/
|
|
468
|
+
export function listRuntimeTeams(): Array<{
|
|
469
|
+
name: string;
|
|
470
|
+
description?: string;
|
|
471
|
+
memberCount: number;
|
|
472
|
+
createdAt?: number;
|
|
473
|
+
}> {
|
|
474
|
+
const teamsDir = path.join(os.homedir(), ".pi", "teams");
|
|
475
|
+
|
|
476
|
+
if (!fs.existsSync(teamsDir)) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const teams: Array<{
|
|
481
|
+
name: string;
|
|
482
|
+
description?: string;
|
|
483
|
+
memberCount: number;
|
|
484
|
+
createdAt?: number;
|
|
485
|
+
}> = [];
|
|
486
|
+
|
|
487
|
+
for (const teamDir of fs.readdirSync(teamsDir, { withFileTypes: true })) {
|
|
488
|
+
if (!teamDir.isDirectory()) continue;
|
|
489
|
+
|
|
490
|
+
const configFile = path.join(teamsDir.path || teamsDir.name, teamDir.name, "config.json");
|
|
491
|
+
const configPath = path.join(os.homedir(), ".pi", "teams", teamDir.name, "config.json");
|
|
492
|
+
|
|
493
|
+
if (fs.existsSync(configPath)) {
|
|
494
|
+
try {
|
|
495
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
496
|
+
teams.push({
|
|
497
|
+
name: config.name || teamDir.name,
|
|
498
|
+
description: config.description,
|
|
499
|
+
memberCount: (config.members || []).filter((m: any) => m.agentType === "teammate").length,
|
|
500
|
+
createdAt: config.createdAt,
|
|
501
|
+
});
|
|
502
|
+
} catch {
|
|
503
|
+
// Skip invalid config files
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return teams;
|
|
261
509
|
}
|