tmux-team 3.0.0 → 3.1.0

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.
@@ -0,0 +1,207 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // install command - install tmux-team for AI agents
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import * as readline from 'node:readline';
9
+ import { fileURLToPath } from 'node:url';
10
+ import type { Context } from '../types.js';
11
+ import { ExitCodes } from '../exits.js';
12
+ import { colors } from '../ui.js';
13
+ import { cmdSetup } from './setup.js';
14
+
15
+ type AgentType = 'claude' | 'codex';
16
+
17
+ interface SkillConfig {
18
+ sourceFile: string;
19
+ userDir: string;
20
+ targetFile: string;
21
+ detected: () => boolean;
22
+ }
23
+
24
+ function getCodexHome(): string {
25
+ return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
26
+ }
27
+
28
+ const SKILL_CONFIGS: Record<AgentType, SkillConfig> = {
29
+ claude: {
30
+ sourceFile: 'skills/claude/team.md',
31
+ userDir: path.join(os.homedir(), '.claude', 'commands'),
32
+ targetFile: 'team.md',
33
+ detected: () => fs.existsSync(path.join(os.homedir(), '.claude')),
34
+ },
35
+ codex: {
36
+ sourceFile: 'skills/codex/SKILL.md',
37
+ userDir: path.join(getCodexHome(), 'skills', 'tmux-team'),
38
+ targetFile: 'SKILL.md',
39
+ detected: () => fs.existsSync(getCodexHome()),
40
+ },
41
+ };
42
+
43
+ const SUPPORTED_AGENTS = Object.keys(SKILL_CONFIGS) as AgentType[];
44
+
45
+ function findPackageRoot(): string {
46
+ const currentFile = fileURLToPath(import.meta.url);
47
+ let dir = path.dirname(currentFile);
48
+
49
+ for (let i = 0; i < 5; i++) {
50
+ const pkgPath = path.join(dir, 'package.json');
51
+ if (fs.existsSync(pkgPath)) {
52
+ return dir;
53
+ }
54
+ const parent = path.dirname(dir);
55
+ if (parent === dir) break;
56
+ dir = parent;
57
+ }
58
+ return path.resolve(path.dirname(currentFile), '..', '..');
59
+ }
60
+
61
+ function detectEnvironment(): AgentType[] {
62
+ const detected: AgentType[] = [];
63
+ for (const [agent, config] of Object.entries(SKILL_CONFIGS)) {
64
+ if (config.detected()) {
65
+ detected.push(agent as AgentType);
66
+ }
67
+ }
68
+ return detected;
69
+ }
70
+
71
+ async function prompt(rl: readline.Interface, question: string): Promise<string> {
72
+ return new Promise((resolve) => {
73
+ rl.question(question, (answer) => {
74
+ resolve(answer.trim());
75
+ });
76
+ });
77
+ }
78
+
79
+ async function confirm(rl: readline.Interface, question: string): Promise<boolean> {
80
+ const answer = await prompt(rl, `${question} [Y/n]: `);
81
+ return answer.toLowerCase() !== 'n';
82
+ }
83
+
84
+ function installSkill(ctx: Context, agent: AgentType): boolean {
85
+ const config = SKILL_CONFIGS[agent];
86
+ const pkgRoot = findPackageRoot();
87
+ const sourcePath = path.join(pkgRoot, config.sourceFile);
88
+
89
+ if (!fs.existsSync(sourcePath)) {
90
+ ctx.ui.error(`Skill file not found: ${sourcePath}`);
91
+ ctx.ui.info('Make sure tmux-team is properly installed.');
92
+ return false;
93
+ }
94
+
95
+ const targetPath = path.join(config.userDir, config.targetFile);
96
+
97
+ if (fs.existsSync(targetPath) && !ctx.flags.force) {
98
+ ctx.ui.warn(`Skill already exists: ${targetPath}`);
99
+ ctx.ui.info('Use --force to overwrite.');
100
+ return false;
101
+ }
102
+
103
+ if (!fs.existsSync(config.userDir)) {
104
+ fs.mkdirSync(config.userDir, { recursive: true });
105
+ }
106
+
107
+ fs.copyFileSync(sourcePath, targetPath);
108
+ return true;
109
+ }
110
+
111
+ export async function cmdInstall(ctx: Context, agent?: string): Promise<void> {
112
+ const { ui, flags, exit } = ctx;
113
+
114
+ const rl = readline.createInterface({
115
+ input: process.stdin,
116
+ output: process.stdout,
117
+ });
118
+
119
+ try {
120
+ let selectedAgent: AgentType;
121
+
122
+ if (agent) {
123
+ // Direct agent specification
124
+ const agentLower = agent.toLowerCase() as AgentType;
125
+ if (!SUPPORTED_AGENTS.includes(agentLower)) {
126
+ ui.error(`Unknown agent: ${agent}`);
127
+ ui.info(`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`);
128
+ exit(ExitCodes.ERROR);
129
+ }
130
+ selectedAgent = agentLower;
131
+ } else {
132
+ // Auto-detect and prompt
133
+ const detected = detectEnvironment();
134
+ console.log();
135
+
136
+ if (detected.length === 0) {
137
+ ui.info('No AI agent environments detected.');
138
+ ui.info(`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`);
139
+ console.log();
140
+ const choice = await prompt(rl, 'Which agent are you using? ');
141
+ const choiceLower = choice.toLowerCase() as AgentType;
142
+ if (!SUPPORTED_AGENTS.includes(choiceLower)) {
143
+ ui.error(`Unknown agent: ${choice}`);
144
+ exit(ExitCodes.ERROR);
145
+ }
146
+ selectedAgent = choiceLower;
147
+ } else if (detected.length === 1) {
148
+ ui.success(`Detected: ${detected[0]}`);
149
+ selectedAgent = detected[0];
150
+ } else {
151
+ ui.info(`Detected multiple environments: ${detected.join(', ')}`);
152
+ const choice = await prompt(rl, 'Which agent are you installing for? ');
153
+ const choiceLower = choice.toLowerCase() as AgentType;
154
+ if (!SUPPORTED_AGENTS.includes(choiceLower)) {
155
+ ui.error(`Unknown agent: ${choice}`);
156
+ exit(ExitCodes.ERROR);
157
+ }
158
+ selectedAgent = choiceLower;
159
+ }
160
+ }
161
+
162
+ // Install the skill
163
+ console.log();
164
+ const success = installSkill(ctx, selectedAgent);
165
+ if (!success) {
166
+ exit(ExitCodes.ERROR);
167
+ }
168
+
169
+ const config = SKILL_CONFIGS[selectedAgent];
170
+ const targetPath = path.join(config.userDir, config.targetFile);
171
+ ui.success(`Installed ${selectedAgent} skill to ${targetPath}`);
172
+
173
+ // Agent-specific instructions
174
+ console.log();
175
+ if (selectedAgent === 'claude') {
176
+ console.log(colors.yellow('For full plugin features, run these in Claude Code:'));
177
+ console.log(` ${colors.cyan('/plugin marketplace add wkh237/tmux-team')}`);
178
+ console.log(` ${colors.cyan('/plugin install tmux-team')}`);
179
+ console.log();
180
+ } else if (selectedAgent === 'codex') {
181
+ console.log(colors.yellow('Enable skills in Codex:'));
182
+ console.log(` ${colors.cyan('codex --enable skills')}`);
183
+ console.log();
184
+ }
185
+
186
+ // Offer to run setup
187
+ if (process.env.TMUX) {
188
+ const runSetup = await confirm(rl, 'Run setup wizard now?');
189
+ if (runSetup) {
190
+ rl.close();
191
+ await cmdSetup(ctx);
192
+ return;
193
+ }
194
+ } else {
195
+ ui.info('Run tmux-team setup inside tmux to configure your agents.');
196
+ }
197
+
198
+ console.log();
199
+ console.log(colors.yellow('Next steps:'));
200
+ console.log(` 1. Start tmux and open panes for your AI agents`);
201
+ console.log(` 2. Run ${colors.cyan('tmux-team setup')} to configure agents`);
202
+ console.log(` 3. Use ${colors.cyan('tmux-team talk <agent> "message" --wait')}`);
203
+ console.log();
204
+ } finally {
205
+ rl.close();
206
+ }
207
+ }
@@ -0,0 +1,80 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // learn command - educational guide for tmux-team
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { colors } from '../ui.js';
6
+
7
+ export function cmdLearn(): void {
8
+ console.log(`
9
+ ${colors.cyan('tmux-team')} - Multi-Agent Coordination Guide
10
+
11
+ ${colors.yellow('WHAT IS TMUX-TEAM?')}
12
+
13
+ tmux-team enables AI agents (Claude, Codex, Gemini) running in separate
14
+ terminal panes to communicate with each other. Think of it as a messaging
15
+ system for terminal-based AI agents.
16
+
17
+ ${colors.yellow('CORE CONCEPT')}
18
+
19
+ Each agent runs in its own tmux pane. When you talk to another agent:
20
+ 1. Your message is sent to their pane via tmux send-keys
21
+ 2. They see it as if a human typed it
22
+ 3. You read their response by capturing their pane output
23
+
24
+ ${colors.yellow('ESSENTIAL COMMANDS')}
25
+
26
+ ${colors.green('tmux-team list')} List available agents
27
+ ${colors.green('tmux-team talk')} <agent> "<msg>" Send a message
28
+ ${colors.green('tmux-team check')} <agent> [lines] Read agent's response
29
+ ${colors.green('tmux-team talk')} <agent> --wait Send and wait for response
30
+
31
+ ${colors.yellow('RECOMMENDED: ASYNC MODE (--wait)')}
32
+
33
+ The ${colors.green('--wait')} flag is recommended for better token utilization:
34
+
35
+ ${colors.dim('# Without --wait (polling mode):')}
36
+ tmux-team talk codex "Review this code"
37
+ ${colors.dim('# ... wait manually ...')}
38
+ tmux-team check codex ${colors.dim('← extra command')}
39
+
40
+ ${colors.dim('# With --wait (async mode):')}
41
+ tmux-team talk codex "Review this code" --wait
42
+ ${colors.dim('↳ Blocks until response, returns it directly')}
43
+
44
+ Enable by default: ${colors.cyan('tmux-team config set mode wait')}
45
+
46
+ ${colors.yellow('PRACTICAL EXAMPLES')}
47
+
48
+ ${colors.dim('# Quick question (async)')}
49
+ tmux-team talk codex "What's the auth status?" --wait
50
+
51
+ ${colors.dim('# Delegate a task with timeout')}
52
+ tmux-team talk gemini "Implement login form" --wait --timeout 300
53
+
54
+ ${colors.dim('# Broadcast to all agents')}
55
+ tmux-team talk all "Sync: PR #123 was merged" --wait
56
+
57
+ ${colors.yellow('CONFIGURATION')}
58
+
59
+ Config file: ${colors.cyan('./tmux-team.json')}
60
+
61
+ {
62
+ "$config": { "mode": "wait" },
63
+ "codex": { "pane": "%1", "remark": "Code reviewer" },
64
+ "gemini": { "pane": "%2", "remark": "Documentation" }
65
+ }
66
+
67
+ Find your pane ID: ${colors.cyan('tmux display-message -p "#{pane_id}"')}
68
+
69
+ ${colors.yellow('BEST PRACTICES')}
70
+
71
+ 1. ${colors.green('Use --wait for important tasks')} - ensures complete response
72
+ 2. ${colors.green('Be explicit')} - tell agents exactly what you need
73
+ 3. ${colors.green('Set timeout appropriately')} - complex tasks need more time
74
+ 4. ${colors.green('Broadcast sparingly')} - only for announcements everyone needs
75
+
76
+ ${colors.yellow('NEXT STEP')}
77
+
78
+ Run ${colors.cyan('tmux-team list')} to see available agents in your project.
79
+ `);
80
+ }
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import type { Context, Flags, Paths, ResolvedConfig, Tmux, UI } from '../types.js';
6
+ import { ExitCodes } from '../exits.js';
7
+
8
+ function createMockUI(): UI {
9
+ return {
10
+ info: vi.fn(),
11
+ success: vi.fn(),
12
+ warn: vi.fn(),
13
+ error: vi.fn(),
14
+ table: vi.fn(),
15
+ json: vi.fn(),
16
+ };
17
+ }
18
+
19
+ function createCtx(testDir: string, tmux: Tmux, flags?: Partial<Flags>): Context {
20
+ const paths: Paths = {
21
+ globalDir: testDir,
22
+ globalConfig: path.join(testDir, 'config.json'),
23
+ localConfig: path.join(testDir, 'tmux-team.json'),
24
+ stateFile: path.join(testDir, 'state.json'),
25
+ };
26
+ const config: ResolvedConfig = {
27
+ mode: 'polling',
28
+ preambleMode: 'always',
29
+ defaults: { timeout: 180, pollInterval: 1, captureLines: 100, preambleEvery: 3 },
30
+ agents: {},
31
+ paneRegistry: {},
32
+ };
33
+ return {
34
+ argv: [],
35
+ flags: { json: false, verbose: false, ...(flags ?? {}) } as Flags,
36
+ ui: createMockUI(),
37
+ config,
38
+ tmux,
39
+ paths,
40
+ exit: ((code: number) => {
41
+ const err = new Error(`exit(${code})`);
42
+ (err as Error & { exitCode: number }).exitCode = code;
43
+ throw err;
44
+ }) as any,
45
+ };
46
+ }
47
+
48
+ describe('cmdSetup', () => {
49
+ let testDir = '';
50
+ const originalTmux = process.env.TMUX;
51
+
52
+ beforeEach(() => {
53
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-setup-'));
54
+ });
55
+
56
+ afterEach(() => {
57
+ process.env.TMUX = originalTmux;
58
+ fs.rmSync(testDir, { recursive: true, force: true });
59
+ vi.restoreAllMocks();
60
+ });
61
+
62
+ it('errors when not in tmux', async () => {
63
+ vi.resetModules();
64
+ delete process.env.TMUX;
65
+ const { cmdSetup } = await import('./setup.js');
66
+ const ctx = createCtx(testDir, {
67
+ send: vi.fn(),
68
+ capture: vi.fn(),
69
+ listPanes: vi.fn(() => []),
70
+ getCurrentPaneId: vi.fn(() => null),
71
+ });
72
+ await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
73
+ });
74
+
75
+ it('creates tmux-team.json by configuring panes', async () => {
76
+ vi.resetModules();
77
+ process.env.TMUX = '1';
78
+
79
+ const answers = [
80
+ '', // accept default "codex" for pane %1
81
+ 'reviewer', // remark
82
+ '1bad', // invalid name for pane %2 -> skipped
83
+ '', // remark (unused)
84
+ ];
85
+
86
+ vi.doMock('readline', () => ({
87
+ default: {
88
+ createInterface: () => ({
89
+ question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
90
+ close: () => {},
91
+ }),
92
+ },
93
+ createInterface: () => ({
94
+ question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
95
+ close: () => {},
96
+ }),
97
+ }));
98
+
99
+ const { cmdSetup } = await import('./setup.js');
100
+ const ctx = createCtx(testDir, {
101
+ send: vi.fn(),
102
+ capture: vi.fn(),
103
+ getCurrentPaneId: vi.fn(() => '%0'),
104
+ listPanes: vi.fn(() => [
105
+ { id: '%0', command: 'zsh', suggestedName: null },
106
+ { id: '%1', command: 'codex', suggestedName: 'codex' },
107
+ { id: '%2', command: 'zsh', suggestedName: null },
108
+ ]),
109
+ });
110
+
111
+ await cmdSetup(ctx);
112
+ const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
113
+ expect(saved.codex.pane).toBe('%1');
114
+ expect(saved.codex.remark).toBe('reviewer');
115
+ });
116
+
117
+ it('errors when no panes found', async () => {
118
+ vi.resetModules();
119
+ process.env.TMUX = '1';
120
+ vi.doMock('readline', () => ({
121
+ default: {
122
+ createInterface: () => ({
123
+ question: (_q: string, cb: (a: string) => void) => cb(''),
124
+ close: () => {},
125
+ }),
126
+ },
127
+ createInterface: () => ({
128
+ question: (_q: string, cb: (a: string) => void) => cb(''),
129
+ close: () => {},
130
+ }),
131
+ }));
132
+ const { cmdSetup } = await import('./setup.js');
133
+ const ctx = createCtx(testDir, {
134
+ send: vi.fn(),
135
+ capture: vi.fn(),
136
+ getCurrentPaneId: vi.fn(() => '%0'),
137
+ listPanes: vi.fn(() => []),
138
+ });
139
+ await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
140
+ });
141
+
142
+ it('exits success when user skips all panes', async () => {
143
+ vi.resetModules();
144
+ process.env.TMUX = '1';
145
+
146
+ const answers = [
147
+ '', // pane %1 has no suggested name -> press Enter to skip
148
+ ];
149
+ vi.doMock('readline', () => ({
150
+ default: {
151
+ createInterface: () => ({
152
+ question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
153
+ close: () => {},
154
+ }),
155
+ },
156
+ createInterface: () => ({
157
+ question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
158
+ close: () => {},
159
+ }),
160
+ }));
161
+
162
+ const { cmdSetup } = await import('./setup.js');
163
+ const ctx = createCtx(testDir, {
164
+ send: vi.fn(),
165
+ capture: vi.fn(),
166
+ getCurrentPaneId: vi.fn(() => '%0'),
167
+ listPanes: vi.fn(() => [
168
+ { id: '%0', command: 'zsh', suggestedName: null },
169
+ { id: '%1', command: 'zsh', suggestedName: null },
170
+ ]),
171
+ });
172
+
173
+ await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.SUCCESS})`);
174
+ });
175
+ });
@@ -0,0 +1,163 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // setup command - interactive wizard for configuring agents
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import fs from 'fs';
6
+ import readline from 'readline';
7
+ import type { Context, PaneEntry, PaneInfo, LocalConfigFile } from '../types.js';
8
+ import { ExitCodes } from '../exits.js';
9
+ import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
10
+ import { colors } from '../ui.js';
11
+
12
+ async function prompt(rl: readline.Interface, question: string): Promise<string> {
13
+ return new Promise((resolve) => {
14
+ rl.question(question, (answer) => {
15
+ resolve(answer.trim());
16
+ });
17
+ });
18
+ }
19
+
20
+ async function promptWithDefault(
21
+ rl: readline.Interface,
22
+ question: string,
23
+ defaultValue: string
24
+ ): Promise<string> {
25
+ const answer = await prompt(rl, `${question} [${defaultValue}]: `);
26
+ return answer || defaultValue;
27
+ }
28
+
29
+ async function confirm(rl: readline.Interface, question: string): Promise<boolean> {
30
+ const answer = await prompt(rl, `${question} [Y/n]: `);
31
+ return answer.toLowerCase() !== 'n';
32
+ }
33
+
34
+ export async function cmdSetup(ctx: Context): Promise<void> {
35
+ const { ui, tmux, paths, exit } = ctx;
36
+
37
+ // Check if in tmux
38
+ if (!process.env.TMUX) {
39
+ ui.error('Not running inside tmux. Please run this command from within a tmux session.');
40
+ exit(ExitCodes.ERROR);
41
+ }
42
+
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: process.stdout,
46
+ });
47
+
48
+ try {
49
+ console.log();
50
+ ui.info('Detecting tmux panes...');
51
+ console.log();
52
+
53
+ const panes = tmux.listPanes();
54
+ const currentPaneId = tmux.getCurrentPaneId();
55
+
56
+ if (panes.length === 0) {
57
+ ui.error('No tmux panes found.');
58
+ exit(ExitCodes.ERROR);
59
+ }
60
+
61
+ // Filter out current pane
62
+ const otherPanes = panes.filter((p) => p.id !== currentPaneId);
63
+
64
+ if (otherPanes.length === 0) {
65
+ ui.warn('No other panes found. Create more tmux panes with other agents first.');
66
+ ui.info('Hint: Use Ctrl+B % or Ctrl+B " to split panes, then start your AI agents.');
67
+ exit(ExitCodes.ERROR);
68
+ }
69
+
70
+ // Show detected panes
71
+ console.log(colors.yellow('Found panes:'));
72
+ console.log(` ${colors.dim(currentPaneId || '?')} ${colors.dim('(current pane - skipped)')}`);
73
+ for (const pane of otherPanes) {
74
+ const detected = pane.suggestedName
75
+ ? colors.green(pane.suggestedName)
76
+ : colors.dim('(unknown)');
77
+ console.log(` ${pane.id} running "${pane.command}" → detected: ${detected}`);
78
+ }
79
+ console.log();
80
+
81
+ // Load existing config
82
+ let localConfig: LocalConfigFile = {};
83
+ if (fs.existsSync(paths.localConfig)) {
84
+ localConfig = loadLocalConfigFile(paths);
85
+ }
86
+
87
+ const agents: Record<string, PaneEntry> = {};
88
+ let configuredCount = 0;
89
+
90
+ // Configure each pane
91
+ for (const pane of otherPanes) {
92
+ const detected = pane.suggestedName || '';
93
+
94
+ console.log(colors.cyan(`Configure pane ${pane.id}?`));
95
+ if (pane.suggestedName) {
96
+ console.log(` Detected: ${colors.green(pane.suggestedName)}`);
97
+ }
98
+
99
+ // Ask for name
100
+ let name: string;
101
+ if (detected) {
102
+ name = await promptWithDefault(rl, ' Name', detected);
103
+ } else {
104
+ name = await prompt(rl, ' Name (or press Enter to skip): ');
105
+ if (!name) {
106
+ console.log(colors.dim(' Skipped'));
107
+ console.log();
108
+ continue;
109
+ }
110
+ }
111
+
112
+ // Validate name
113
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
114
+ ui.warn(
115
+ ' Invalid name. Use letters, numbers, underscores, hyphens. Starting with a letter.'
116
+ );
117
+ console.log(colors.dim(' Skipped'));
118
+ console.log();
119
+ continue;
120
+ }
121
+
122
+ // Ask for optional remark
123
+ const remark = await prompt(rl, ' Remark (optional): ');
124
+
125
+ const entry: PaneEntry = { pane: pane.id };
126
+ if (remark) {
127
+ entry.remark = remark;
128
+ }
129
+
130
+ agents[name] = entry;
131
+ configuredCount++;
132
+ console.log();
133
+ }
134
+
135
+ if (configuredCount === 0) {
136
+ ui.warn('No agents configured.');
137
+ exit(ExitCodes.SUCCESS);
138
+ }
139
+
140
+ // Merge with existing config (preserve $config and existing agents)
141
+ const newConfig: LocalConfigFile = { ...localConfig };
142
+ for (const [name, entry] of Object.entries(agents)) {
143
+ newConfig[name] = entry;
144
+ }
145
+
146
+ // Save config
147
+ if (!fs.existsSync(paths.localConfig)) {
148
+ fs.writeFileSync(paths.localConfig, '{}\n');
149
+ }
150
+ saveLocalConfigFile(paths, newConfig);
151
+
152
+ ui.success(`Created ${paths.localConfig} with ${configuredCount} agent(s)`);
153
+ console.log();
154
+
155
+ // Show next steps
156
+ const firstAgent = Object.keys(agents)[0];
157
+ console.log(colors.yellow('Try it:'));
158
+ console.log(` ${colors.cyan(`tmux-team talk ${firstAgent} "hello" --wait`)}`);
159
+ console.log();
160
+ } finally {
161
+ rl.close();
162
+ }
163
+ }