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 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:
@@ -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: tmux, Zellij, or iTerm2
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 **iTerm2** (macOS).
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.7.3",
3
+ "version": "0.8.2",
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": [
@@ -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. iTerm2 - if TERM_PROGRAM=iTerm.app and not in tmux/zellij
33
- * 3. Zellij - if ZELLIJ env is set and not in tmux
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
+ }