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 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 Zellij.
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: tmux, Zellij, or iTerm2
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 **iTerm2** (macOS).
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** will use AppleScript to automatically split your current window into an optimized layout (1 large Lead pane on the left, Teammates stacked on the right). It will also name the panes with the teammate's agent name for easy identification.
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
 
@@ -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
- // Use a shorter, more prominent status at the beginning if possible
31
- ctx.ui.setStatus("00-pi-teams", `[${agentName.toUpperCase()}]`);
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
- terminal.setTitle(agentName);
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
- // If config can't be read, that's okay - proceed without model info
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 if process already dead
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. Ensure you're running in tmux, iTerm2, or Zellij.");
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"; // Assumed on path
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
- // For iTerm2, we need to handle the spawn context for proper layout
210
- if (terminal instanceof Iterm2Adapter) {
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
- paneId = terminal.spawn({
223
- name: safeName,
224
- cwd: params.cwd,
225
- command: piCmd,
226
- env: env,
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 ${paneId}.` }],
237
- details: { agentId: member.agentId, paneId },
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 tasks.",
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: "team_delete",
394
- label: "Delete Team",
395
- description: "Delete a team and all its data.",
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
- if (member.name !== "team-lead") {
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
- // config might not exist, ignore
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: "task_get",
441
- label: "Get Task",
442
- description: "Get full details of a specific task by ID.",
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.tmuxPaneId && terminal) {
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 (!member) throw new Error(`Teammate ${params.agent_name} not found`);
518
-
519
- await killTeammate(params.team_name, member);
520
-
521
- await teams.removeMember(params.team_name, params.agent_name);
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.7.3",
3
+ "version": "0.8.5",
4
4
  "description": "Agent teams for pi, ported from claude-code-teams-mcp",
5
- "repository": "github:burggraf/pi-teams",
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": [