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 CHANGED
@@ -97,28 +97,125 @@ tmt ls
97
97
  tmt rm codex
98
98
  ```
99
99
 
100
- ## Claude Code Plugin
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
- Gives you two slash commands:
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
- **`/learn`** - Teach Claude how to use tmux-team
197
+ ### Real-World Examples
198
+
199
+ **Code review delegation:**
110
200
  ```
111
- /learn
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
- **`/team`** - Talk to other agents
204
+ **Cross-agent coordination:**
116
205
  ```
117
- /team talk codex "Review my authentication changes"
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
- Use this to delegate tasks, ask for reviews, or broadcast updates.
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.0",
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);
@@ -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
- ui.info(`Created ${paths.localConfig}`);
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
- ui.success(`Added agent '${name}' at pane ${pane}`);
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
  }
@@ -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)')}
@@ -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
- ui.success(`Created ${paths.localConfig}`);
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
  }
@@ -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
- ui.info("No agents configured. Use 'tmux-team add <name> <pane>' to add one.");
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 || '-'])
@@ -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
- export function resolvePaths(cwd: string = process.cwd()): Paths {
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 payload = ensureTrailingNewline(message);
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
@@ -64,6 +64,7 @@ export interface Flags {
64
64
  timeout?: number; // seconds
65
65
  lines?: number; // lines to capture before end marker
66
66
  noPreamble?: boolean;
67
+ team?: string; // shared team name for cross-folder collaboration
67
68
  }
68
69
 
69
70
  export interface Paths {