pi-teams 0.7.3 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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:
|
|
@@ -87,9 +89,9 @@ Teammates in `planning` mode will use `task_submit_plan`. As the lead, review th
|
|
|
87
89
|
- **[Full Usage Guide](docs/guide.md)** - Detailed examples, hook system, best practices, and troubleshooting
|
|
88
90
|
- **[Tool Reference](docs/reference.md)** - Complete documentation of all tools and parameters
|
|
89
91
|
|
|
90
|
-
## 🪟 Terminal Requirements
|
|
92
|
+
## 🪟 Terminal Requirements
|
|
91
93
|
|
|
92
|
-
To show multiple agents on one screen, **pi-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, and **
|
|
94
|
+
To show multiple agents on one screen, **pi-teams** requires a way to manage terminal panes. It supports **tmux**, **Zellij**, **iTerm2**, and **WezTerm**.
|
|
93
95
|
|
|
94
96
|
### Option 1: tmux (Recommended)
|
|
95
97
|
|
|
@@ -111,6 +113,21 @@ Simply start `pi` inside a Zellij session. **pi-teams** will detect it via the `
|
|
|
111
113
|
|
|
112
114
|
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.
|
|
113
115
|
|
|
116
|
+
### Option 4: WezTerm (macOS, Linux, Windows)
|
|
117
|
+
|
|
118
|
+
**WezTerm** is a GPU-accelerated, cross-platform terminal emulator written in Rust. If you are using WezTerm and are *not* inside tmux or Zellij, **pi-teams** will use `wezterm cli split-pane` to spawn teammates in new panes with an optimized layout (1 large Lead pane on the left, Teammates stacked on the right).
|
|
119
|
+
|
|
120
|
+
Install WezTerm:
|
|
121
|
+
- **macOS**: `brew install --cask wezterm`
|
|
122
|
+
- **Linux**: See [wezterm.org/installation](https://wezterm.org/installation)
|
|
123
|
+
- **Windows**: Download from [wezterm.org](https://wezterm.org)
|
|
124
|
+
|
|
125
|
+
How to run:
|
|
126
|
+
```bash
|
|
127
|
+
wezterm # Start WezTerm
|
|
128
|
+
pi # Start pi inside WezTerm
|
|
129
|
+
```
|
|
130
|
+
|
|
114
131
|
## 📜 Credits & Attribution
|
|
115
132
|
|
|
116
133
|
This project is a port of the excellent [claude-code-teams-mcp](https://github.com/cs50victor/claude-code-teams-mcp) by [cs50victor](https://github.com/cs50victor).
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-teams",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
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": [
|
|
@@ -9,14 +9,22 @@ import { TerminalAdapter } from "../utils/terminal-adapter";
|
|
|
9
9
|
import { TmuxAdapter } from "./tmux-adapter";
|
|
10
10
|
import { Iterm2Adapter } from "./iterm2-adapter";
|
|
11
11
|
import { ZellijAdapter } from "./zellij-adapter";
|
|
12
|
+
import { WezTermAdapter } from "./wezterm-adapter";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Available terminal adapters, ordered by priority
|
|
16
|
+
*
|
|
17
|
+
* Detection order (first match wins):
|
|
18
|
+
* 1. tmux - if TMUX env is set
|
|
19
|
+
* 2. Zellij - if ZELLIJ env is set and not in tmux
|
|
20
|
+
* 3. iTerm2 - if TERM_PROGRAM=iTerm.app and not in tmux/zellij
|
|
21
|
+
* 4. WezTerm - if WEZTERM_PANE env is set and not in tmux/zellij
|
|
15
22
|
*/
|
|
16
23
|
const adapters: TerminalAdapter[] = [
|
|
17
24
|
new TmuxAdapter(),
|
|
18
|
-
new Iterm2Adapter(),
|
|
19
25
|
new ZellijAdapter(),
|
|
26
|
+
new Iterm2Adapter(),
|
|
27
|
+
new WezTermAdapter(),
|
|
20
28
|
];
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -26,12 +34,13 @@ let cachedAdapter: TerminalAdapter | null = null;
|
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* Detect and return the appropriate terminal adapter for the current environment.
|
|
29
|
-
*
|
|
37
|
+
*
|
|
30
38
|
* Detection order (first match wins):
|
|
31
39
|
* 1. tmux - if TMUX env is set
|
|
32
|
-
* 2.
|
|
33
|
-
* 3.
|
|
34
|
-
*
|
|
40
|
+
* 2. Zellij - if ZELLIJ env is set and not in tmux
|
|
41
|
+
* 3. iTerm2 - if TERM_PROGRAM=iTerm.app and not in tmux/zellij
|
|
42
|
+
* 4. WezTerm - if WEZTERM_PANE env is set and not in tmux/zellij
|
|
43
|
+
*
|
|
35
44
|
* @returns The detected terminal adapter, or null if none detected
|
|
36
45
|
*/
|
|
37
46
|
export function getTerminalAdapter(): TerminalAdapter | null {
|
|
@@ -51,8 +60,8 @@ export function getTerminalAdapter(): TerminalAdapter | null {
|
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
62
|
* Get a specific terminal adapter by name.
|
|
54
|
-
*
|
|
55
|
-
* @param name - The adapter name (e.g., "tmux", "iTerm2", "zellij")
|
|
63
|
+
*
|
|
64
|
+
* @param name - The adapter name (e.g., "tmux", "iTerm2", "zellij", "WezTerm")
|
|
56
65
|
* @returns The adapter instance, or undefined if not found
|
|
57
66
|
*/
|
|
58
67
|
export function getAdapterByName(name: string): TerminalAdapter | undefined {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WezTerm Adapter Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import { WezTermAdapter } from "./wezterm-adapter";
|
|
7
|
+
import * as terminalAdapter from "../utils/terminal-adapter";
|
|
8
|
+
|
|
9
|
+
describe("WezTermAdapter", () => {
|
|
10
|
+
let adapter: WezTermAdapter;
|
|
11
|
+
let mockExecCommand: ReturnType<typeof vi.spyOn>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
adapter = new WezTermAdapter();
|
|
15
|
+
mockExecCommand = vi.spyOn(terminalAdapter, "execCommand");
|
|
16
|
+
delete process.env.WEZTERM_PANE;
|
|
17
|
+
delete process.env.TMUX;
|
|
18
|
+
delete process.env.ZELLIJ;
|
|
19
|
+
process.env.WEZTERM_PANE = "0";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("name", () => {
|
|
27
|
+
it("should have the correct name", () => {
|
|
28
|
+
expect(adapter.name).toBe("WezTerm");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("detect", () => {
|
|
33
|
+
it("should detect when WEZTERM_PANE is set", () => {
|
|
34
|
+
mockExecCommand.mockReturnValue({ stdout: "version 1.0", stderr: "", status: 0 });
|
|
35
|
+
expect(adapter.detect()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("spawn", () => {
|
|
40
|
+
it("should spawn first pane to the right with 50%", () => {
|
|
41
|
+
// Mock getPanes finding only current pane
|
|
42
|
+
mockExecCommand.mockImplementation((bin, args) => {
|
|
43
|
+
if (args.includes("list")) {
|
|
44
|
+
return {
|
|
45
|
+
stdout: JSON.stringify([{ pane_id: 0, tab_id: 0 }]),
|
|
46
|
+
stderr: "",
|
|
47
|
+
status: 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (args.includes("split-pane")) {
|
|
51
|
+
return { stdout: "1", stderr: "", status: 0 };
|
|
52
|
+
}
|
|
53
|
+
return { stdout: "", stderr: "", status: 0 };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = adapter.spawn({
|
|
57
|
+
name: "test-agent",
|
|
58
|
+
cwd: "/home/user/project",
|
|
59
|
+
command: "pi --agent test",
|
|
60
|
+
env: { PI_AGENT_ID: "test-123" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toBe("wezterm_1");
|
|
64
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining("wezterm"),
|
|
66
|
+
expect.arrayContaining(["cli", "split-pane", "--right", "--percent", "50"])
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should spawn subsequent panes by splitting the sidebar", () => {
|
|
71
|
+
// Mock getPanes finding current pane (0) and sidebar pane (1)
|
|
72
|
+
mockExecCommand.mockImplementation((bin, args) => {
|
|
73
|
+
if (args.includes("list")) {
|
|
74
|
+
return {
|
|
75
|
+
stdout: JSON.stringify([{ pane_id: 0, tab_id: 0 }, { pane_id: 1, tab_id: 0 }]),
|
|
76
|
+
stderr: "",
|
|
77
|
+
status: 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (args.includes("split-pane")) {
|
|
81
|
+
return { stdout: "2", stderr: "", status: 0 };
|
|
82
|
+
}
|
|
83
|
+
return { stdout: "", stderr: "", status: 0 };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = adapter.spawn({
|
|
87
|
+
name: "agent2",
|
|
88
|
+
cwd: "/home/user/project",
|
|
89
|
+
command: "pi",
|
|
90
|
+
env: {},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toBe("wezterm_2");
|
|
94
|
+
// 1 sidebar pane already exists, so percent should be floor(100/(1+1)) = 50%
|
|
95
|
+
expect(mockExecCommand).toHaveBeenCalledWith(
|
|
96
|
+
expect.stringContaining("wezterm"),
|
|
97
|
+
expect.arrayContaining(["cli", "split-pane", "--bottom", "--pane-id", "1", "--percent", "50"])
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WezTerm Terminal Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the TerminalAdapter interface for WezTerm terminal emulator.
|
|
5
|
+
* Uses wezterm cli split-pane for pane management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TerminalAdapter, SpawnOptions, execCommand } from "../utils/terminal-adapter";
|
|
9
|
+
|
|
10
|
+
export class WezTermAdapter implements TerminalAdapter {
|
|
11
|
+
readonly name = "WezTerm";
|
|
12
|
+
|
|
13
|
+
// Common paths where wezterm CLI might be found
|
|
14
|
+
private possiblePaths = [
|
|
15
|
+
"wezterm", // In PATH
|
|
16
|
+
"/Applications/WezTerm.app/Contents/MacOS/wezterm", // macOS
|
|
17
|
+
"/usr/local/bin/wezterm", // Linux/macOS common
|
|
18
|
+
"/usr/bin/wezterm", // Linux system
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
private weztermPath: string | null = null;
|
|
22
|
+
|
|
23
|
+
private findWeztermBinary(): string | null {
|
|
24
|
+
if (this.weztermPath !== null) {
|
|
25
|
+
return this.weztermPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const path of this.possiblePaths) {
|
|
29
|
+
try {
|
|
30
|
+
const result = execCommand(path, ["--version"]);
|
|
31
|
+
if (result.status === 0) {
|
|
32
|
+
this.weztermPath = path;
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Continue to next path
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.weztermPath = null;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
detect(): boolean {
|
|
45
|
+
if (!process.env.WEZTERM_PANE || process.env.TMUX || process.env.ZELLIJ) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return this.findWeztermBinary() !== null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all panes in the current tab to determine layout state.
|
|
53
|
+
*/
|
|
54
|
+
private getPanes(): any[] {
|
|
55
|
+
const weztermBin = this.findWeztermBinary();
|
|
56
|
+
if (!weztermBin) return [];
|
|
57
|
+
|
|
58
|
+
const result = execCommand(weztermBin, ["cli", "list", "--format", "json"]);
|
|
59
|
+
if (result.status !== 0) return [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const allPanes = JSON.parse(result.stdout);
|
|
63
|
+
const currentPaneId = parseInt(process.env.WEZTERM_PANE || "0", 10);
|
|
64
|
+
|
|
65
|
+
// Find the tab of the current pane
|
|
66
|
+
const currentPane = allPanes.find((p: any) => p.pane_id === currentPaneId);
|
|
67
|
+
if (!currentPane) return [];
|
|
68
|
+
|
|
69
|
+
// Return all panes in the same tab
|
|
70
|
+
return allPanes.filter((p: any) => p.tab_id === currentPane.tab_id);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
spawn(options: SpawnOptions): string {
|
|
77
|
+
const weztermBin = this.findWeztermBinary();
|
|
78
|
+
if (!weztermBin) {
|
|
79
|
+
throw new Error("WezTerm CLI binary not found.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const panes = this.getPanes();
|
|
83
|
+
const envArgs = Object.entries(options.env)
|
|
84
|
+
.filter(([k]) => k.startsWith("PI_"))
|
|
85
|
+
.map(([k, v]) => `${k}=${v}`);
|
|
86
|
+
|
|
87
|
+
let weztermArgs: string[];
|
|
88
|
+
|
|
89
|
+
// First pane: split to the right with 50% (matches iTerm2/tmux behavior)
|
|
90
|
+
const isFirstPane = panes.length === 1;
|
|
91
|
+
|
|
92
|
+
if (isFirstPane) {
|
|
93
|
+
weztermArgs = [
|
|
94
|
+
"cli", "split-pane", "--right", "--percent", "50",
|
|
95
|
+
"--cwd", options.cwd, "--", "env", ...envArgs, "sh", "-c", options.command
|
|
96
|
+
];
|
|
97
|
+
} else {
|
|
98
|
+
// Subsequent teammates stack in the sidebar on the right.
|
|
99
|
+
// currentPaneId (id 0) is the main pane on the left.
|
|
100
|
+
// All other panes are in the sidebar.
|
|
101
|
+
const currentPaneId = parseInt(process.env.WEZTERM_PANE || "0", 10);
|
|
102
|
+
const sidebarPanes = panes
|
|
103
|
+
.filter(p => p.pane_id !== currentPaneId)
|
|
104
|
+
.sort((a, b) => b.cursor_y - a.cursor_y); // Sort by vertical position (bottom-most first)
|
|
105
|
+
|
|
106
|
+
// To add a new pane to the bottom of the sidebar stack:
|
|
107
|
+
// We always split the BOTTOM-MOST pane (sidebarPanes[0])
|
|
108
|
+
// and use 50% so the new pane and the previous bottom pane are equal.
|
|
109
|
+
// This progressively fills the sidebar from top to bottom.
|
|
110
|
+
const targetPane = sidebarPanes[0];
|
|
111
|
+
|
|
112
|
+
weztermArgs = [
|
|
113
|
+
"cli", "split-pane", "--bottom", "--pane-id", targetPane.pane_id.toString(),
|
|
114
|
+
"--percent", "50",
|
|
115
|
+
"--cwd", options.cwd, "--", "env", ...envArgs, "sh", "-c", options.command
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = execCommand(weztermBin, weztermArgs);
|
|
120
|
+
if (result.status !== 0) {
|
|
121
|
+
throw new Error(`wezterm spawn failed: ${result.stderr}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// New: After spawning, tell WezTerm to equalize the panes in this tab
|
|
125
|
+
// This ensures that regardless of the split math, they all end up the same height.
|
|
126
|
+
try {
|
|
127
|
+
execCommand(weztermBin, ["cli", "zoom-pane", "--unzoom"]); // Ensure not zoomed
|
|
128
|
+
// WezTerm doesn't have a single "equalize" command like tmux,
|
|
129
|
+
// but splitting with no percentage usually balances, or we can use
|
|
130
|
+
// the 'AdjustPaneSize' sequence.
|
|
131
|
+
// For now, let's stick to the 50/50 split of the LAST pane which is most reliable.
|
|
132
|
+
} catch {}
|
|
133
|
+
|
|
134
|
+
const paneId = result.stdout.trim();
|
|
135
|
+
return `wezterm_${paneId}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
kill(paneId: string): void {
|
|
139
|
+
if (!paneId?.startsWith("wezterm_")) return;
|
|
140
|
+
const weztermBin = this.findWeztermBinary();
|
|
141
|
+
if (!weztermBin) return;
|
|
142
|
+
|
|
143
|
+
const weztermId = paneId.replace("wezterm_", "");
|
|
144
|
+
try {
|
|
145
|
+
execCommand(weztermBin, ["cli", "kill-pane", "--pane-id", weztermId]);
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isAlive(paneId: string): boolean {
|
|
150
|
+
if (!paneId?.startsWith("wezterm_")) return false;
|
|
151
|
+
const weztermBin = this.findWeztermBinary();
|
|
152
|
+
if (!weztermBin) return false;
|
|
153
|
+
|
|
154
|
+
const weztermId = parseInt(paneId.replace("wezterm_", ""), 10);
|
|
155
|
+
const panes = this.getPanes();
|
|
156
|
+
return panes.some(p => p.pane_id === weztermId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setTitle(title: string): void {
|
|
160
|
+
const weztermBin = this.findWeztermBinary();
|
|
161
|
+
if (!weztermBin) return;
|
|
162
|
+
try {
|
|
163
|
+
execCommand(weztermBin, ["cli", "set-tab-title", title]);
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|