pi-teams 0.7.3 → 0.8.5
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 +38 -4
- package/extensions/index.ts +135 -123
- package/package.json +5 -2
- package/src/adapters/iterm2-adapter.ts +162 -20
- package/src/adapters/terminal-registry.ts +35 -7
- package/src/adapters/tmux-adapter.ts +35 -0
- package/src/adapters/wezterm-adapter.test.ts +101 -0
- package/src/adapters/wezterm-adapter.ts +304 -0
- package/src/adapters/zellij-adapter.ts +35 -0
- package/src/utils/models.ts +2 -0
- package/src/utils/teams.ts +3 -1
- package/src/utils/terminal-adapter.ts +49 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-teams 🚀
|
|
2
2
|
|
|
3
|
-
**pi-teams** turns your single Pi agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board—all mediated through tmux, iTerm2, or
|
|
3
|
+
**pi-teams** turns your single Pi agent into a coordinated software engineering team. It allows you to spawn multiple "Teammate" agents in separate terminal panes that work autonomously, communicate with each other, and manage a shared task board—all mediated through tmux, Zellij, iTerm2, or WezTerm.
|
|
4
4
|
|
|
5
5
|
### 🖥️ pi-teams in Action
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
| :---: | :---: | :---: |
|
|
9
9
|
| <a href="iTerm2.png"><img src="iTerm2.png" width="300" alt="pi-teams in iTerm2"></a> | <a href="tmux.png"><img src="tmux.png" width="300" alt="pi-teams in tmux"></a> | <a href="zellij.png"><img src="zellij.png" width="300" alt="pi-teams in Zellij"></a> |
|
|
10
10
|
|
|
11
|
+
*Also works with **WezTerm** (cross-platform support)*
|
|
12
|
+
|
|
11
13
|
## 🛠 Installation
|
|
12
14
|
|
|
13
15
|
Open your Pi terminal and type:
|
|
@@ -43,6 +45,8 @@ pi install npm:pi-teams
|
|
|
43
45
|
- **Beautiful UI**: Optimized vertical splits in `tmux` with clear labels so you always know who is doing what.
|
|
44
46
|
|
|
45
47
|
### Advanced Features
|
|
48
|
+
- **Isolated OS Windows**: Launch teammates in true separate OS windows instead of panes.
|
|
49
|
+
- **Persistent Window Titles**: Windows are automatically titled `[team-name]: [agent-name]` for easy identification in your window manager.
|
|
46
50
|
- **Plan Approval Mode**: Require teammates to submit their implementation plans for your approval before they touch any code.
|
|
47
51
|
- **Broadcast Messaging**: Send a message to the entire team at once for global coordination and announcements.
|
|
48
52
|
- **Quality Gate Hooks**: Automated shell scripts run when tasks are completed (e.g., to run tests or linting).
|
|
@@ -56,9 +60,20 @@ pi install npm:pi-teams
|
|
|
56
60
|
**Set a default model for the whole team:**
|
|
57
61
|
> **You:** "Create a team named 'Research' and use 'gpt-4o' for everyone."
|
|
58
62
|
|
|
63
|
+
**Start a team in "Separate Windows" mode:**
|
|
64
|
+
> **You:** "Create a team named 'Dev' and open everyone in separate windows."
|
|
65
|
+
*(Supported in iTerm2 and WezTerm only)*
|
|
66
|
+
|
|
59
67
|
### 2. Spawn Teammate with Custom Settings
|
|
60
68
|
> **You:** "Spawn a teammate named 'security-bot' in the current folder. Tell them to scan for hardcoded API keys."
|
|
61
69
|
|
|
70
|
+
**Spawn a specific teammate in a separate window:**
|
|
71
|
+
> **You:** "Spawn 'researcher' in a separate window."
|
|
72
|
+
|
|
73
|
+
**Move the Team Lead to a separate window:**
|
|
74
|
+
> **You:** "Open the team lead in its own window."
|
|
75
|
+
*(Requires separate_windows mode enabled or iTerm2/WezTerm)*
|
|
76
|
+
|
|
62
77
|
**Use a different model:**
|
|
63
78
|
> **You:** "Spawn a teammate named 'speed-bot' using 'haiku' to quickly run some benchmarks."
|
|
64
79
|
|
|
@@ -87,9 +102,9 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
|
|
87
102
|
- **[Full Usage Guide](docs/guide.md)** - Detailed examples, hook system, best practices, and troubleshooting
|
|
88
103
|
- **[Tool Reference](docs/reference.md)** - Complete documentation of all tools and parameters
|
|
89
104
|
|
|
90
|
-
## 🪟 Terminal Requirements
|
|
105
|
+
## 🪟 Terminal Requirements
|
|
91
106
|
|
|
92
|
-
To show multiple agents on one screen, **pi-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, and **
|
|
107
|
+
To show multiple agents on one screen, **pi-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, **iTerm2**, and **WezTerm**.
|
|
93
108
|
|
|
94
109
|
### Option 1: tmux (Recommended)
|
|
95
110
|
|
|
@@ -109,7 +124,26 @@ Simply start `pi` inside a Zellij session. **pi-teams** will detect it via the `
|
|
|
109
124
|
|
|
110
125
|
### Option 3: iTerm2 (macOS)
|
|
111
126
|
|
|
112
|
-
If you are using **iTerm2** on macOS and are *not* inside tmux or Zellij, **pi-teams**
|
|
127
|
+
If you are using **iTerm2** on macOS and are *not* inside tmux or Zellij, **pi-teams** can manage your team in two ways:
|
|
128
|
+
1. **Panes (Default)**: Automatically split your current window into an optimized layout.
|
|
129
|
+
2. **Windows**: Create true separate OS windows for each agent.
|
|
130
|
+
|
|
131
|
+
It will name the panes or windows with the teammate's agent name for easy identification.
|
|
132
|
+
|
|
133
|
+
### Option 4: WezTerm (macOS, Linux, Windows)
|
|
134
|
+
|
|
135
|
+
**WezTerm** is a GPU-accelerated, cross-platform terminal emulator written in Rust. Like iTerm2, it supports both **Panes** and **Separate OS Windows**.
|
|
136
|
+
|
|
137
|
+
Install WezTerm:
|
|
138
|
+
- **macOS**: `brew install --cask wezterm`
|
|
139
|
+
- **Linux**: See [wezterm.org/installation](https://wezterm.org/installation)
|
|
140
|
+
- **Windows**: Download from [wezterm.org](https://wezterm.org)
|
|
141
|
+
|
|
142
|
+
How to run:
|
|
143
|
+
```bash
|
|
144
|
+
wezterm # Start WezTerm
|
|
145
|
+
pi # Start pi inside WezTerm
|
|
146
|
+
```
|
|
113
147
|
|
|
114
148
|
## 📜 Credits & Attribution
|
|
115
149
|
|
package/extensions/index.ts
CHANGED
|
@@ -8,15 +8,14 @@ import * as messaging from "../src/utils/messaging";
|
|
|
8
8
|
import { Member } from "../src/utils/models";
|
|
9
9
|
import { getTerminalAdapter } from "../src/adapters/terminal-registry";
|
|
10
10
|
import { Iterm2Adapter } from "../src/adapters/iterm2-adapter";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
13
|
|
|
14
14
|
export default function (pi: ExtensionAPI) {
|
|
15
15
|
const isTeammate = !!process.env.PI_AGENT_NAME;
|
|
16
16
|
const agentName = process.env.PI_AGENT_NAME || "team-lead";
|
|
17
17
|
const teamName = process.env.PI_TEAM_NAME;
|
|
18
18
|
|
|
19
|
-
// Get the terminal adapter once at startup
|
|
20
19
|
const terminal = getTerminalAdapter();
|
|
21
20
|
|
|
22
21
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -27,20 +26,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
27
26
|
fs.writeFileSync(pidFile, process.pid.toString());
|
|
28
27
|
}
|
|
29
28
|
ctx.ui.notify(`Teammate: ${agentName} (Team: ${teamName})`, "info");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Set the terminal pane title for better visibility
|
|
29
|
+
ctx.ui.setStatus("00-pi-teams", `[${agentName.toUpperCase()}]`);
|
|
30
|
+
|
|
34
31
|
if (terminal) {
|
|
35
|
-
|
|
32
|
+
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
|
33
|
+
const setIt = () => {
|
|
34
|
+
if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
|
|
35
|
+
terminal.setTitle(fullTitle);
|
|
36
|
+
};
|
|
37
|
+
setIt();
|
|
38
|
+
setTimeout(setIt, 500);
|
|
39
|
+
setTimeout(setIt, 2000);
|
|
40
|
+
setTimeout(setIt, 5000);
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
// Auto-trigger the first turn for teammates
|
|
42
|
+
|
|
39
43
|
setTimeout(() => {
|
|
40
44
|
pi.sendUserMessage(`I am starting my work as '${agentName}' on team '${teamName}'. Checking my inbox for instructions...`);
|
|
41
45
|
}, 1000);
|
|
42
46
|
|
|
43
|
-
// Periodically check for new messages when idle
|
|
44
47
|
setInterval(async () => {
|
|
45
48
|
if (ctx.isIdle() && teamName) {
|
|
46
49
|
const unread = await messaging.readInbox(teamName, agentName, true, false);
|
|
@@ -54,12 +57,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
54
57
|
}
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
61
|
+
if (isTeammate) {
|
|
62
|
+
const fullTitle = teamName ? `${teamName}: ${agentName}` : agentName;
|
|
63
|
+
if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
|
|
64
|
+
if (terminal) terminal.setTitle(fullTitle);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
57
68
|
let firstTurn = true;
|
|
58
69
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
59
70
|
if (isTeammate && firstTurn) {
|
|
60
71
|
firstTurn = false;
|
|
61
72
|
|
|
62
|
-
// Get the teammate's model and thinking level from team config for accurate reporting
|
|
63
73
|
let modelInfo = "";
|
|
64
74
|
if (teamName) {
|
|
65
75
|
try {
|
|
@@ -73,7 +83,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
83
|
modelInfo += `. When reporting your model or thinking level, use these exact values.`;
|
|
74
84
|
}
|
|
75
85
|
} catch (e) {
|
|
76
|
-
//
|
|
86
|
+
// Ignore
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -93,10 +103,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
93
103
|
process.kill(parseInt(pid), "SIGKILL");
|
|
94
104
|
fs.unlinkSync(pidFile);
|
|
95
105
|
} catch (e) {
|
|
96
|
-
// ignore
|
|
106
|
+
// ignore
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
|
|
110
|
+
if (member.windowId && terminal) {
|
|
111
|
+
terminal.killWindow(member.windowId);
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
if (member.tmuxPaneId && terminal) {
|
|
101
115
|
terminal.kill(member.tmuxPaneId);
|
|
102
116
|
}
|
|
@@ -111,9 +125,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
125
|
team_name: Type.String(),
|
|
112
126
|
description: Type.Optional(Type.String()),
|
|
113
127
|
default_model: Type.Optional(Type.String()),
|
|
128
|
+
separate_windows: Type.Optional(Type.Boolean({ default: false, description: "Open teammates in separate OS windows instead of panes" })),
|
|
114
129
|
}),
|
|
115
130
|
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
116
|
-
const config = teams.createTeam(params.team_name, "local-session", "lead-agent", params.description, params.default_model);
|
|
131
|
+
const config = teams.createTeam(params.team_name, "local-session", "lead-agent", params.description, params.default_model, params.separate_windows);
|
|
117
132
|
return {
|
|
118
133
|
content: [{ type: "text", text: `Team ${params.team_name} created.` }],
|
|
119
134
|
details: { config },
|
|
@@ -124,7 +139,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
124
139
|
pi.registerTool({
|
|
125
140
|
name: "spawn_teammate",
|
|
126
141
|
label: "Spawn Teammate",
|
|
127
|
-
description: "Spawn a new teammate in a terminal pane.",
|
|
142
|
+
description: "Spawn a new teammate in a terminal pane or separate window.",
|
|
128
143
|
parameters: Type.Object({
|
|
129
144
|
team_name: Type.String(),
|
|
130
145
|
name: Type.String(),
|
|
@@ -133,6 +148,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
133
148
|
model: Type.Optional(Type.String()),
|
|
134
149
|
thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high"])),
|
|
135
150
|
plan_mode_required: Type.Optional(Type.Boolean({ default: false })),
|
|
151
|
+
separate_window: Type.Optional(Type.Boolean({ default: false })),
|
|
136
152
|
}),
|
|
137
153
|
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
138
154
|
const safeName = paths.sanitizeName(params.name);
|
|
@@ -143,20 +159,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
if (!terminal) {
|
|
146
|
-
throw new Error("No terminal adapter detected.
|
|
162
|
+
throw new Error("No terminal adapter detected.");
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
const teamConfig = await teams.readConfig(safeTeamName);
|
|
150
166
|
let chosenModel = params.model || teamConfig.defaultModel;
|
|
151
167
|
|
|
152
|
-
// If model doesn't include provider prefix (provider/model), use the team's defaultModel or fallback
|
|
153
168
|
if (chosenModel && !chosenModel.includes('/')) {
|
|
154
|
-
// Check if team has a defaultModel with a provider prefix
|
|
155
169
|
if (teamConfig.defaultModel && teamConfig.defaultModel.includes('/')) {
|
|
156
170
|
const [provider] = teamConfig.defaultModel.split('/');
|
|
157
171
|
chosenModel = `${provider}/${chosenModel}`;
|
|
158
172
|
} else {
|
|
159
|
-
// Infer provider from model name
|
|
160
173
|
if (chosenModel.startsWith('glm-')) {
|
|
161
174
|
chosenModel = `zai/${chosenModel}`;
|
|
162
175
|
} else if (chosenModel.startsWith('claude-')) {
|
|
@@ -165,6 +178,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
const useSeparateWindow = params.separate_window ?? teamConfig.separateWindows ?? false;
|
|
182
|
+
if (useSeparateWindow && !terminal.supportsWindows()) {
|
|
183
|
+
throw new Error(`Separate windows mode is not supported in ${terminal.name}.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
168
186
|
const member: Member = {
|
|
169
187
|
agentId: `${safeName}@${safeTeamName}`,
|
|
170
188
|
name: safeName,
|
|
@@ -183,14 +201,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
201
|
await teams.addMember(safeTeamName, member);
|
|
184
202
|
await messaging.sendPlainMessage(safeTeamName, "team-lead", safeName, params.prompt, "Initial prompt");
|
|
185
203
|
|
|
186
|
-
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
204
|
+
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
187
205
|
let piCmd = piBinary;
|
|
188
206
|
|
|
189
|
-
// Build model command with thinking level if specified
|
|
190
207
|
if (chosenModel) {
|
|
191
208
|
const [provider, ...modelParts] = chosenModel.split('/');
|
|
192
209
|
const modelName = modelParts.join('/');
|
|
193
|
-
|
|
194
210
|
if (params.thinking) {
|
|
195
211
|
piCmd = `${piBinary} --provider ${provider} --model ${modelName}:${params.thinking}`;
|
|
196
212
|
} else {
|
|
@@ -206,39 +222,83 @@ export default function (pi: ExtensionAPI) {
|
|
|
206
222
|
PI_AGENT_NAME: safeName,
|
|
207
223
|
};
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const teammates = teamConfig.members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
|
|
212
|
-
const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
|
|
213
|
-
if (lastTeammate?.tmuxPaneId) {
|
|
214
|
-
terminal.setSpawnContext({ lastSessionId: lastTeammate.tmuxPaneId.replace("iterm_", "") });
|
|
215
|
-
} else {
|
|
216
|
-
terminal.setSpawnContext({});
|
|
217
|
-
}
|
|
218
|
-
}
|
|
225
|
+
let terminalId = "";
|
|
226
|
+
let isWindow = false;
|
|
219
227
|
|
|
220
|
-
let paneId = "";
|
|
221
228
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
if (useSeparateWindow) {
|
|
230
|
+
isWindow = true;
|
|
231
|
+
terminalId = terminal.spawnWindow({
|
|
232
|
+
name: safeName,
|
|
233
|
+
cwd: params.cwd,
|
|
234
|
+
command: piCmd,
|
|
235
|
+
env: env,
|
|
236
|
+
teamName: safeTeamName,
|
|
237
|
+
});
|
|
238
|
+
await teams.updateMember(safeTeamName, safeName, { windowId: terminalId });
|
|
239
|
+
} else {
|
|
240
|
+
if (terminal instanceof Iterm2Adapter) {
|
|
241
|
+
const teammates = teamConfig.members.filter(m => m.agentType === "teammate" && m.tmuxPaneId.startsWith("iterm_"));
|
|
242
|
+
const lastTeammate = teammates.length > 0 ? teammates[teammates.length - 1] : null;
|
|
243
|
+
if (lastTeammate?.tmuxPaneId) {
|
|
244
|
+
terminal.setSpawnContext({ lastSessionId: lastTeammate.tmuxPaneId.replace("iterm_", "") });
|
|
245
|
+
} else {
|
|
246
|
+
terminal.setSpawnContext({});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
terminalId = terminal.spawn({
|
|
251
|
+
name: safeName,
|
|
252
|
+
cwd: params.cwd,
|
|
253
|
+
command: piCmd,
|
|
254
|
+
env: env,
|
|
255
|
+
});
|
|
256
|
+
await teams.updateMember(safeTeamName, safeName, { tmuxPaneId: terminalId });
|
|
257
|
+
}
|
|
228
258
|
} catch (e) {
|
|
229
|
-
throw new Error(`Failed to spawn ${terminal.name} pane: ${e}`);
|
|
259
|
+
throw new Error(`Failed to spawn ${terminal.name} ${isWindow ? 'window' : 'pane'}: ${e}`);
|
|
230
260
|
}
|
|
231
261
|
|
|
232
|
-
// Update member with paneId
|
|
233
|
-
await teams.updateMember(params.team_name, params.name, { tmuxPaneId: paneId });
|
|
234
|
-
|
|
235
262
|
return {
|
|
236
|
-
content: [{ type: "text", text: `Teammate ${params.name} spawned in pane ${
|
|
237
|
-
details: { agentId: member.agentId,
|
|
263
|
+
content: [{ type: "text", text: `Teammate ${params.name} spawned in ${isWindow ? 'window' : 'pane'} ${terminalId}.` }],
|
|
264
|
+
details: { agentId: member.agentId, terminalId, isWindow },
|
|
238
265
|
};
|
|
239
266
|
},
|
|
240
267
|
});
|
|
241
268
|
|
|
269
|
+
pi.registerTool({
|
|
270
|
+
name: "spawn_lead_window",
|
|
271
|
+
label: "Spawn Lead Window",
|
|
272
|
+
description: "Open the team lead in a separate OS window.",
|
|
273
|
+
parameters: Type.Object({
|
|
274
|
+
team_name: Type.String(),
|
|
275
|
+
cwd: Type.Optional(Type.String()),
|
|
276
|
+
}),
|
|
277
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
278
|
+
const safeTeamName = paths.sanitizeName(params.team_name);
|
|
279
|
+
if (!teams.teamExists(safeTeamName)) throw new Error(`Team ${params.team_name} does not exist`);
|
|
280
|
+
if (!terminal || !terminal.supportsWindows()) throw new Error("Windows mode not supported.");
|
|
281
|
+
|
|
282
|
+
const teamConfig = await teams.readConfig(safeTeamName);
|
|
283
|
+
const cwd = params.cwd || process.cwd();
|
|
284
|
+
const piBinary = process.argv[1] ? `node ${process.argv[1]}` : "pi";
|
|
285
|
+
let piCmd = piBinary;
|
|
286
|
+
if (teamConfig.defaultModel) {
|
|
287
|
+
const [provider, ...modelParts] = teamConfig.defaultModel.split('/');
|
|
288
|
+
piCmd = `${piBinary} --provider ${provider} --model ${modelParts.join('/')}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const env = { ...process.env, PI_TEAM_NAME: safeTeamName, PI_AGENT_NAME: "team-lead" };
|
|
292
|
+
try {
|
|
293
|
+
const windowId = terminal.spawnWindow({ name: "team-lead", cwd, command: piCmd, env, teamName: safeTeamName });
|
|
294
|
+
await teams.updateMember(safeTeamName, "team-lead", { windowId });
|
|
295
|
+
return { content: [{ type: "text", text: `Lead window spawned: ${windowId}` }], details: { windowId } };
|
|
296
|
+
} catch (e) {
|
|
297
|
+
throw new Error(`Failed: ${e}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
242
302
|
pi.registerTool({
|
|
243
303
|
name: "send_message",
|
|
244
304
|
label: "Send Message",
|
|
@@ -286,7 +346,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
346
|
agent_name: Type.Optional(Type.String({ description: "Whose inbox to read. Defaults to your own." })),
|
|
287
347
|
unread_only: Type.Optional(Type.Boolean({ default: true })),
|
|
288
348
|
}),
|
|
289
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
349
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
290
350
|
const targetAgent = params.agent_name || agentName;
|
|
291
351
|
const msgs = await messaging.readInbox(params.team_name, targetAgent, params.unread_only);
|
|
292
352
|
return {
|
|
@@ -305,7 +365,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
305
365
|
subject: Type.String(),
|
|
306
366
|
description: Type.String(),
|
|
307
367
|
}),
|
|
308
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
368
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
309
369
|
const task = await tasks.createTask(params.team_name, params.subject, params.description);
|
|
310
370
|
return {
|
|
311
371
|
content: [{ type: "text", text: `Task ${task.id} created.` }],
|
|
@@ -354,11 +414,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
354
414
|
pi.registerTool({
|
|
355
415
|
name: "task_list",
|
|
356
416
|
label: "List Tasks",
|
|
357
|
-
description: "List all team
|
|
417
|
+
description: "List all tasks for a team.",
|
|
358
418
|
parameters: Type.Object({
|
|
359
419
|
team_name: Type.String(),
|
|
360
420
|
}),
|
|
361
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
421
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
362
422
|
const taskList = await tasks.listTasks(params.team_name);
|
|
363
423
|
return {
|
|
364
424
|
content: [{ type: "text", text: JSON.stringify(taskList, null, 2) }],
|
|
@@ -390,61 +450,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
390
450
|
});
|
|
391
451
|
|
|
392
452
|
pi.registerTool({
|
|
393
|
-
name: "
|
|
394
|
-
label: "
|
|
395
|
-
description: "
|
|
453
|
+
name: "team_shutdown",
|
|
454
|
+
label: "Shutdown Team",
|
|
455
|
+
description: "Shutdown the entire team and close all panes/windows.",
|
|
396
456
|
parameters: Type.Object({
|
|
397
457
|
team_name: Type.String(),
|
|
398
458
|
}),
|
|
399
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
459
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
400
460
|
const teamName = params.team_name;
|
|
401
461
|
try {
|
|
402
462
|
const config = await teams.readConfig(teamName);
|
|
403
463
|
for (const member of config.members) {
|
|
404
|
-
|
|
405
|
-
ctx.ui.notify(`Stopping teammate: ${member.name}`, "info");
|
|
406
|
-
await killTeammate(teamName, member);
|
|
407
|
-
}
|
|
464
|
+
await killTeammate(teamName, member);
|
|
408
465
|
}
|
|
466
|
+
const dir = paths.teamDir(teamName);
|
|
467
|
+
const tasksDir = paths.taskDir(teamName);
|
|
468
|
+
if (fs.existsSync(tasksDir)) fs.rmSync(tasksDir, { recursive: true });
|
|
469
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
470
|
+
return { content: [{ type: "text", text: `Team ${teamName} shut down.` }], details: {} };
|
|
409
471
|
} catch (e) {
|
|
410
|
-
|
|
472
|
+
throw new Error(`Failed to shutdown team: ${e}`);
|
|
411
473
|
}
|
|
412
|
-
|
|
413
|
-
const dir = paths.teamDir(teamName);
|
|
414
|
-
const tasksDir = paths.taskDir(teamName);
|
|
415
|
-
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
416
|
-
if (fs.existsSync(tasksDir)) fs.rmSync(tasksDir, { recursive: true });
|
|
417
|
-
return {
|
|
418
|
-
content: [{ type: "text", text: `Team ${teamName} deleted.` }],
|
|
419
|
-
};
|
|
420
|
-
},
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
pi.registerTool({
|
|
424
|
-
name: "read_config",
|
|
425
|
-
label: "Read Config",
|
|
426
|
-
description: "Read the current team configuration.",
|
|
427
|
-
parameters: Type.Object({
|
|
428
|
-
team_name: Type.String(),
|
|
429
|
-
}),
|
|
430
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
431
|
-
const config = await teams.readConfig(params.team_name);
|
|
432
|
-
return {
|
|
433
|
-
content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
|
|
434
|
-
details: { config },
|
|
435
|
-
};
|
|
436
474
|
},
|
|
437
475
|
});
|
|
438
476
|
|
|
439
477
|
pi.registerTool({
|
|
440
|
-
name: "
|
|
441
|
-
label: "
|
|
442
|
-
description: "
|
|
478
|
+
name: "task_read",
|
|
479
|
+
label: "Read Task",
|
|
480
|
+
description: "Read details of a specific task.",
|
|
443
481
|
parameters: Type.Object({
|
|
444
482
|
team_name: Type.String(),
|
|
445
483
|
task_id: Type.String(),
|
|
446
484
|
}),
|
|
447
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
485
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
448
486
|
const task = await tasks.readTask(params.team_name, params.task_id);
|
|
449
487
|
return {
|
|
450
488
|
content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
|
|
@@ -453,29 +491,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
453
491
|
},
|
|
454
492
|
});
|
|
455
493
|
|
|
456
|
-
pi.registerTool({
|
|
457
|
-
name: "force_kill_teammate",
|
|
458
|
-
label: "Force Kill Teammate",
|
|
459
|
-
description: "Forcibly kill a teammate's terminal pane.",
|
|
460
|
-
parameters: Type.Object({
|
|
461
|
-
team_name: Type.String(),
|
|
462
|
-
agent_name: Type.String(),
|
|
463
|
-
}),
|
|
464
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
465
|
-
const config = await teams.readConfig(params.team_name);
|
|
466
|
-
const member = config.members.find(m => m.name === params.agent_name);
|
|
467
|
-
if (!member) throw new Error(`Teammate ${params.agent_name} not found`);
|
|
468
|
-
|
|
469
|
-
await killTeammate(params.team_name, member);
|
|
470
|
-
|
|
471
|
-
await teams.removeMember(params.team_name, params.agent_name);
|
|
472
|
-
await tasks.resetOwnerTasks(params.team_name, params.agent_name);
|
|
473
|
-
return {
|
|
474
|
-
content: [{ type: "text", text: `${params.agent_name} has been stopped.` }],
|
|
475
|
-
};
|
|
476
|
-
},
|
|
477
|
-
});
|
|
478
|
-
|
|
479
494
|
pi.registerTool({
|
|
480
495
|
name: "check_teammate",
|
|
481
496
|
label: "Check Teammate",
|
|
@@ -484,18 +499,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
484
499
|
team_name: Type.String(),
|
|
485
500
|
agent_name: Type.String(),
|
|
486
501
|
}),
|
|
487
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
502
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
488
503
|
const config = await teams.readConfig(params.team_name);
|
|
489
504
|
const member = config.members.find(m => m.name === params.agent_name);
|
|
490
505
|
if (!member) throw new Error(`Teammate ${params.agent_name} not found`);
|
|
491
506
|
|
|
492
507
|
let alive = false;
|
|
493
|
-
if (member.
|
|
508
|
+
if (member.windowId && terminal) {
|
|
509
|
+
alive = terminal.isWindowAlive(member.windowId);
|
|
510
|
+
} else if (member.tmuxPaneId && terminal) {
|
|
494
511
|
alive = terminal.isAlive(member.tmuxPaneId);
|
|
495
512
|
}
|
|
496
513
|
|
|
497
514
|
const unreadCount = (await messaging.readInbox(params.team_name, params.agent_name, true, false)).length;
|
|
498
|
-
|
|
499
515
|
return {
|
|
500
516
|
content: [{ type: "text", text: JSON.stringify({ alive, unreadCount }, null, 2) }],
|
|
501
517
|
details: { alive, unreadCount },
|
|
@@ -511,18 +527,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
511
527
|
team_name: Type.String(),
|
|
512
528
|
agent_name: Type.String(),
|
|
513
529
|
}),
|
|
514
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
530
|
+
async execute(toolCallId, params: any, signal, onUpdate, ctx) {
|
|
515
531
|
const config = await teams.readConfig(params.team_name);
|
|
516
532
|
const member = config.members.find(m => m.name === params.agent_name);
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await tasks.resetOwnerTasks(params.team_name, params.agent_name);
|
|
523
|
-
return {
|
|
524
|
-
content: [{ type: "text", text: `${params.agent_name} removed from team.` }],
|
|
525
|
-
};
|
|
533
|
+
if (member) {
|
|
534
|
+
await killTeammate(params.team_name, member);
|
|
535
|
+
await teams.removeMember(params.team_name, params.agent_name);
|
|
536
|
+
}
|
|
537
|
+
return { content: [{ type: "text", text: `Teammate ${params.agent_name} shutdown processed.` }], details: {} };
|
|
526
538
|
},
|
|
527
539
|
});
|
|
528
540
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-teams",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Agent teams for pi, ported from claude-code-teams-mcp",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/burggraf/pi-teams.git"
|
|
8
|
+
},
|
|
6
9
|
"author": "Mark Burggraf",
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"keywords": [
|