tmux-team 4.0.0-beta.0 → 4.0.0-beta.1
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 +107 -10
- package/package.json +16 -15
- package/src/cli.ts +4 -0
- package/src/commands/add.ts +17 -4
- package/src/commands/help.ts +2 -0
- package/src/commands/init.ts +13 -3
- package/src/commands/list.ts +10 -2
- package/src/commands/this.ts +1 -1
- package/src/config.ts +30 -1
- package/src/context.ts +1 -1
- package/src/tmux.ts +7 -1
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -97,28 +97,125 @@ tmt ls
|
|
|
97
97
|
tmt rm codex
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Shared Teams
|
|
103
|
+
|
|
104
|
+
> *Work on different folders but talk to the same team of agents.*
|
|
105
|
+
|
|
106
|
+
By default, `tmux-team.json` is local to each folder. The `--team` flag lets agents across different folders share a team:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Initialize a shared team
|
|
110
|
+
tmt init --team myproject
|
|
111
|
+
|
|
112
|
+
# Register agents from ANY folder
|
|
113
|
+
cd ~/code/frontend && tmt this claude --team myproject
|
|
114
|
+
cd ~/code/backend && tmt this codex --team myproject
|
|
115
|
+
cd ~/code/infra && tmt this gemini --team myproject
|
|
116
|
+
|
|
117
|
+
# Now talk to them from anywhere
|
|
118
|
+
tmt talk codex "What's the user API schema?" --team myproject
|
|
119
|
+
tmt talk all "Starting deploy - heads up" --team myproject
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **Tip:** Most AI coding agents (Claude Code, Codex, Gemini CLI) support `!` to run shell commands. Agents can register themselves without leaving the session:
|
|
123
|
+
> ```
|
|
124
|
+
> !tmt this claude --team myproject
|
|
125
|
+
> ```
|
|
126
|
+
|
|
127
|
+
### When to use shared teams
|
|
128
|
+
|
|
129
|
+
**Single project** (default) — agents work in the same folder:
|
|
130
|
+
```bash
|
|
131
|
+
tmt init
|
|
132
|
+
tmt this claude
|
|
133
|
+
tmt add codex 1.1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Shared team** — agents work across folders but collaborate:
|
|
137
|
+
```bash
|
|
138
|
+
tmt init --team acme-app
|
|
139
|
+
tmt this frontend-claude --team acme-app # from ~/acme/frontend
|
|
140
|
+
tmt this backend-codex --team acme-app # from ~/acme/backend
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Multi-team coordination
|
|
144
|
+
|
|
145
|
+
For large systems, create team hierarchies where leaders coordinate sub-teams:
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart
|
|
149
|
+
|
|
150
|
+
A["you (claude)"]
|
|
151
|
+
A2["codex"]
|
|
152
|
+
A3["gemini"]
|
|
153
|
+
B["backend-lead"]
|
|
154
|
+
B2["codex"]
|
|
155
|
+
C["infra-lead"]
|
|
156
|
+
C2["codex"]
|
|
157
|
+
|
|
158
|
+
subgraph your-team
|
|
159
|
+
A <--> A2
|
|
160
|
+
A <--> A3
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
A e1@<--> B
|
|
164
|
+
A e2@<--> C
|
|
165
|
+
|
|
166
|
+
e1@{ animate: true }
|
|
167
|
+
e2@{ animate: true }
|
|
168
|
+
|
|
169
|
+
subgraph backend-team
|
|
170
|
+
B <--> B2
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
subgraph infra-team
|
|
174
|
+
C <--> C2
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Using /team in Claude Code
|
|
181
|
+
|
|
182
|
+
The `/team` command lets Claude talk to other AI agents directly. Install the plugin:
|
|
101
183
|
|
|
102
184
|
```
|
|
103
185
|
/plugin marketplace add wkh237/tmux-team
|
|
104
186
|
/plugin install tmux-team
|
|
105
187
|
```
|
|
106
188
|
|
|
107
|
-
|
|
189
|
+
### /team Commands
|
|
190
|
+
|
|
191
|
+
| Command | What it does |
|
|
192
|
+
|---------|--------------|
|
|
193
|
+
| `/team list` | Show all registered agents |
|
|
194
|
+
| `/team talk <agent> "msg"` | Send a message and wait for response |
|
|
195
|
+
| `/team talk all "msg"` | Broadcast to all agents |
|
|
108
196
|
|
|
109
|
-
|
|
197
|
+
### Real-World Examples
|
|
198
|
+
|
|
199
|
+
**Code review delegation:**
|
|
110
200
|
```
|
|
111
|
-
/
|
|
201
|
+
/team talk codex "Review my changes in src/auth/ for security issues"
|
|
112
202
|
```
|
|
113
|
-
Run this once when starting a session. Claude will understand how to coordinate with other agents.
|
|
114
203
|
|
|
115
|
-
|
|
204
|
+
**Cross-agent coordination:**
|
|
116
205
|
```
|
|
117
|
-
/team talk
|
|
118
|
-
/team talk all "I'm starting the database migration"
|
|
119
|
-
/team list
|
|
206
|
+
/team talk all "Starting database migration - hold off on API changes"
|
|
120
207
|
```
|
|
121
|
-
|
|
208
|
+
|
|
209
|
+
**Ask a specialist:**
|
|
210
|
+
```
|
|
211
|
+
/team talk gemini "What's the best practice for rate limiting in GCP?"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Tips
|
|
215
|
+
|
|
216
|
+
- `/team talk` waits for the agent to respond before continuing
|
|
217
|
+
- Use `/team list` to see who's available
|
|
218
|
+
- Run `/learn` once per session to teach Claude the full tmux-team workflow
|
|
122
219
|
|
|
123
220
|
## Learn More
|
|
124
221
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-team",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.1",
|
|
4
4
|
"description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tmux-team": "./bin/tmux-team",
|
|
8
8
|
"tmt": "./bin/tmux-team"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx src/cli.ts",
|
|
12
|
+
"tmt": "./bin/tmux-team",
|
|
13
|
+
"test": "pnpm test:run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 90 --branches 85",
|
|
16
|
+
"lint": "oxlint src/",
|
|
17
|
+
"lint:fix": "oxlint src/ --fix",
|
|
18
|
+
"format": "prettier --write src/",
|
|
19
|
+
"format:check": "prettier --check src/",
|
|
20
|
+
"type:check": "tsc --noEmit",
|
|
21
|
+
"check": "pnpm type:check && pnpm lint && pnpm format:check"
|
|
22
|
+
},
|
|
10
23
|
"keywords": [
|
|
11
24
|
"tmux",
|
|
12
25
|
"cli",
|
|
@@ -24,6 +37,7 @@
|
|
|
24
37
|
"engines": {
|
|
25
38
|
"node": ">=18"
|
|
26
39
|
},
|
|
40
|
+
"packageManager": "pnpm@9.15.4",
|
|
27
41
|
"os": [
|
|
28
42
|
"darwin",
|
|
29
43
|
"linux"
|
|
@@ -43,18 +57,5 @@
|
|
|
43
57
|
"prettier": "^3.7.4",
|
|
44
58
|
"typescript": "^5.3.0",
|
|
45
59
|
"vitest": "^1.2.0"
|
|
46
|
-
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"dev": "tsx src/cli.ts",
|
|
49
|
-
"tmt": "./bin/tmux-team",
|
|
50
|
-
"test": "pnpm test:run",
|
|
51
|
-
"test:watch": "vitest",
|
|
52
|
-
"test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 90 --branches 85",
|
|
53
|
-
"lint": "oxlint src/",
|
|
54
|
-
"lint:fix": "oxlint src/ --fix",
|
|
55
|
-
"format": "prettier --write src/",
|
|
56
|
-
"format:check": "prettier --check src/",
|
|
57
|
-
"type:check": "tsc --noEmit",
|
|
58
|
-
"check": "pnpm type:check && pnpm lint && pnpm format:check"
|
|
59
60
|
}
|
|
60
|
-
}
|
|
61
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -59,6 +59,10 @@ function parseArgs(argv: string[]): { command: string; args: string[]; flags: Fl
|
|
|
59
59
|
flags.lines = parseInt(argv[++i], 10) || 100;
|
|
60
60
|
} else if (arg === '--no-preamble') {
|
|
61
61
|
flags.noPreamble = true;
|
|
62
|
+
} else if (arg === '--team') {
|
|
63
|
+
flags.team = argv[++i];
|
|
64
|
+
} else if (arg.startsWith('--team=')) {
|
|
65
|
+
flags.team = arg.slice(7);
|
|
62
66
|
} else if (arg.startsWith('--pane=')) {
|
|
63
67
|
// Handled in update command
|
|
64
68
|
positional.push(arg);
|
package/src/commands/add.ts
CHANGED
|
@@ -5,16 +5,25 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import type { Context, PaneEntry } from '../types.js';
|
|
7
7
|
import { ExitCodes } from '../exits.js';
|
|
8
|
-
import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
|
|
8
|
+
import { loadLocalConfigFile, saveLocalConfigFile, ensureTeamsDir } from '../config.js';
|
|
9
9
|
|
|
10
10
|
export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string): void {
|
|
11
11
|
const { ui, config, paths, flags, exit } = ctx;
|
|
12
12
|
|
|
13
|
+
// Ensure teams directory exists if using --team
|
|
14
|
+
if (flags.team) {
|
|
15
|
+
ensureTeamsDir(paths.globalDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
// Create config file if it doesn't exist
|
|
14
19
|
if (!fs.existsSync(paths.localConfig)) {
|
|
15
20
|
fs.writeFileSync(paths.localConfig, '{}\n');
|
|
16
21
|
if (!flags.json) {
|
|
17
|
-
|
|
22
|
+
if (flags.team) {
|
|
23
|
+
ui.info(`Created shared team "${flags.team}"`);
|
|
24
|
+
} else {
|
|
25
|
+
ui.info(`Created ${paths.localConfig}`);
|
|
26
|
+
}
|
|
18
27
|
}
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -35,8 +44,12 @@ export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string
|
|
|
35
44
|
saveLocalConfigFile(paths, localConfig);
|
|
36
45
|
|
|
37
46
|
if (flags.json) {
|
|
38
|
-
ui.json({ added: name, pane, remark });
|
|
47
|
+
ui.json({ added: name, pane, remark, team: flags.team });
|
|
39
48
|
} else {
|
|
40
|
-
|
|
49
|
+
if (flags.team) {
|
|
50
|
+
ui.success(`Added agent '${name}' to team "${flags.team}" at pane ${pane}`);
|
|
51
|
+
} else {
|
|
52
|
+
ui.success(`Added agent '${name}' at pane ${pane}`);
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
55
|
}
|
package/src/commands/help.ts
CHANGED
|
@@ -64,6 +64,7 @@ ${colors.yellow('OPTIONS')}
|
|
|
64
64
|
${colors.green('--json')} Output in JSON format
|
|
65
65
|
${colors.green('--verbose')} Show detailed output
|
|
66
66
|
${colors.green('--force')} Skip warnings
|
|
67
|
+
${colors.green('--team')} <name> Use shared team (cross-folder)
|
|
67
68
|
|
|
68
69
|
${colors.yellow('TALK OPTIONS')}
|
|
69
70
|
${colors.green('--delay')} <seconds> Wait before sending
|
|
@@ -90,6 +91,7 @@ ${colors.yellow('EXAMPLES')}${
|
|
|
90
91
|
${colors.yellow('CONFIG')}
|
|
91
92
|
Local: ./tmux-team.json (pane registry + $config override)
|
|
92
93
|
Global: ~/.config/tmux-team/config.json (settings)
|
|
94
|
+
Teams: ~/.config/tmux-team/teams/<name>.json (shared teams)
|
|
93
95
|
|
|
94
96
|
${colors.yellow('CHANGE MODE')}
|
|
95
97
|
tmux-team config set mode wait ${colors.dim('Enable wait mode (local)')}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// init command - create tmux-team.json
|
|
2
|
+
// init command - create tmux-team.json or shared team config
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import type { Context } from '../types.js';
|
|
7
7
|
import { ExitCodes } from '../exits.js';
|
|
8
|
+
import { ensureTeamsDir } from '../config.js';
|
|
8
9
|
|
|
9
10
|
export function cmdInit(ctx: Context): void {
|
|
10
11
|
const { ui, paths, flags, exit } = ctx;
|
|
11
12
|
|
|
13
|
+
// Ensure teams directory exists if using --team
|
|
14
|
+
if (flags.team) {
|
|
15
|
+
ensureTeamsDir(paths.globalDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
if (fs.existsSync(paths.localConfig)) {
|
|
13
19
|
ui.error(`${paths.localConfig} already exists. Remove it first if you want to reinitialize.`);
|
|
14
20
|
exit(ExitCodes.ERROR);
|
|
@@ -17,8 +23,12 @@ export function cmdInit(ctx: Context): void {
|
|
|
17
23
|
fs.writeFileSync(paths.localConfig, '{}\n');
|
|
18
24
|
|
|
19
25
|
if (flags.json) {
|
|
20
|
-
ui.json({ created: paths.localConfig });
|
|
26
|
+
ui.json({ created: paths.localConfig, team: flags.team });
|
|
21
27
|
} else {
|
|
22
|
-
|
|
28
|
+
if (flags.team) {
|
|
29
|
+
ui.success(`Created shared team "${flags.team}" at ${paths.localConfig}`);
|
|
30
|
+
} else {
|
|
31
|
+
ui.success(`Created ${paths.localConfig}`);
|
|
32
|
+
}
|
|
23
33
|
}
|
|
24
34
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -9,16 +9,24 @@ export function cmdList(ctx: Context): void {
|
|
|
9
9
|
const agents = Object.entries(config.paneRegistry);
|
|
10
10
|
|
|
11
11
|
if (flags.json) {
|
|
12
|
-
ui.json(config.paneRegistry);
|
|
12
|
+
ui.json({ team: flags.team, agents: config.paneRegistry });
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
if (agents.length === 0) {
|
|
17
|
-
|
|
17
|
+
if (flags.team) {
|
|
18
|
+
ui.info(`No agents in team "${flags.team}". Use 'tmt this <name> --team ${flags.team}' to add one.`);
|
|
19
|
+
} else {
|
|
20
|
+
ui.info("No agents configured. Use 'tmux-team add <name> <pane>' to add one.");
|
|
21
|
+
}
|
|
18
22
|
return;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
console.log();
|
|
26
|
+
if (flags.team) {
|
|
27
|
+
console.log(`Team: ${flags.team}`);
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
22
30
|
ui.table(
|
|
23
31
|
['NAME', 'PANE', 'REMARK'],
|
|
24
32
|
agents.map(([name, data]) => [name, data.pane, data.remark || '-'])
|
package/src/commands/this.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function cmdThis(ctx: Context, name: string, remark?: string): void {
|
|
|
12
12
|
const currentPaneId = tmux.getCurrentPaneId();
|
|
13
13
|
if (!currentPaneId) {
|
|
14
14
|
ui.error('Not running inside tmux.');
|
|
15
|
-
exit(ExitCodes.ERROR);
|
|
15
|
+
return exit(ExitCodes.ERROR);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Reuse existing add logic
|
package/src/config.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
const CONFIG_FILENAME = 'config.json';
|
|
18
18
|
const LOCAL_CONFIG_FILENAME = 'tmux-team.json';
|
|
19
19
|
const STATE_FILENAME = 'state.json';
|
|
20
|
+
const TEAMS_DIR = 'teams';
|
|
20
21
|
|
|
21
22
|
// Default configuration values
|
|
22
23
|
const DEFAULT_CONFIG: GlobalConfig = {
|
|
@@ -102,9 +103,37 @@ function findUpward(filename: string, startDir: string): string | null {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Get the path to a shared team config file.
|
|
108
|
+
*/
|
|
109
|
+
export function getTeamConfigPath(globalDir: string, teamName: string): string {
|
|
110
|
+
return path.join(globalDir, TEAMS_DIR, `${teamName}.json`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ensure the teams directory exists.
|
|
115
|
+
*/
|
|
116
|
+
export function ensureTeamsDir(globalDir: string): void {
|
|
117
|
+
const teamsDir = path.join(globalDir, TEAMS_DIR);
|
|
118
|
+
if (!fs.existsSync(teamsDir)) {
|
|
119
|
+
fs.mkdirSync(teamsDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolvePaths(cwd: string = process.cwd(), teamName?: string): Paths {
|
|
106
124
|
const globalDir = resolveGlobalDir();
|
|
107
125
|
|
|
126
|
+
// If team name is specified, use the shared team config
|
|
127
|
+
if (teamName) {
|
|
128
|
+
const teamConfig = getTeamConfigPath(globalDir, teamName);
|
|
129
|
+
return {
|
|
130
|
+
globalDir,
|
|
131
|
+
globalConfig: path.join(globalDir, CONFIG_FILENAME),
|
|
132
|
+
localConfig: teamConfig,
|
|
133
|
+
stateFile: path.join(globalDir, STATE_FILENAME),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
// Search up for local config (like .git discovery)
|
|
109
138
|
const localConfigPath =
|
|
110
139
|
findUpward(LOCAL_CONFIG_FILENAME, cwd) ?? path.join(cwd, LOCAL_CONFIG_FILENAME);
|
package/src/context.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface CreateContextOptions {
|
|
|
17
17
|
export function createContext(options: CreateContextOptions): Context {
|
|
18
18
|
const { argv, flags, cwd = process.cwd() } = options;
|
|
19
19
|
|
|
20
|
-
const paths = resolvePaths(cwd);
|
|
20
|
+
const paths = resolvePaths(cwd, flags.team);
|
|
21
21
|
const config = loadConfig(paths);
|
|
22
22
|
const ui = createUI(flags.json);
|
|
23
23
|
const tmux = createTmux();
|
package/src/tmux.ts
CHANGED
|
@@ -39,6 +39,11 @@ export function createTmux(): Tmux {
|
|
|
39
39
|
return message.endsWith('\n') ? message : `${message}\n`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function escapeExclamation(message: string): string {
|
|
43
|
+
// Escape "!" to avoid shell history expansion issues
|
|
44
|
+
return message.replace(/!/g, '\\!');
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
function makeBufferName(): string {
|
|
43
48
|
const nonce = crypto.randomBytes(4).toString('hex');
|
|
44
49
|
return `tmt-${process.pid}-${Date.now()}-${nonce}`;
|
|
@@ -48,7 +53,8 @@ export function createTmux(): Tmux {
|
|
|
48
53
|
send(paneId: string, message: string, options?: { enterDelayMs?: number }): void {
|
|
49
54
|
const enterDelayMs = Math.max(0, options?.enterDelayMs ?? 500);
|
|
50
55
|
const bufferName = makeBufferName();
|
|
51
|
-
const
|
|
56
|
+
const escaped = escapeExclamation(message);
|
|
57
|
+
const payload = ensureTrailingNewline(escaped);
|
|
52
58
|
|
|
53
59
|
try {
|
|
54
60
|
execSync(`tmux set-buffer -b "${bufferName}" -- ${JSON.stringify(payload)}`, {
|
package/src/types.ts
CHANGED