tmux-team 1.0.0 → 2.0.0-alpha.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/src/cli.ts ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env tsx
2
+ // ─────────────────────────────────────────────────────────────
3
+ // tmux-team CLI entry point
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import { createContext, ExitCodes } from './context.js';
7
+ import type { Flags } from './types.js';
8
+
9
+ // Commands
10
+ import { cmdHelp } from './commands/help.js';
11
+ import { cmdInit } from './commands/init.js';
12
+ import { cmdList } from './commands/list.js';
13
+ import { cmdAdd } from './commands/add.js';
14
+ import { cmdUpdate } from './commands/update.js';
15
+ import { cmdRemove } from './commands/remove.js';
16
+ import { cmdTalk } from './commands/talk.js';
17
+ import { cmdCheck } from './commands/check.js';
18
+ import { cmdCompletion } from './commands/completion.js';
19
+ import { cmdPm } from './pm/commands.js';
20
+
21
+ // ─────────────────────────────────────────────────────────────
22
+ // Argument parsing
23
+ // ─────────────────────────────────────────────────────────────
24
+
25
+ function parseArgs(argv: string[]): { command: string; args: string[]; flags: Flags } {
26
+ const flags: Flags = {
27
+ json: false,
28
+ verbose: false,
29
+ };
30
+
31
+ const positional: string[] = [];
32
+ let i = 0;
33
+
34
+ while (i < argv.length) {
35
+ const arg = argv[i];
36
+
37
+ if (arg === '--json') {
38
+ flags.json = true;
39
+ } else if (arg === '--verbose' || arg === '-v') {
40
+ flags.verbose = true;
41
+ } else if (arg === '--force' || arg === '-f') {
42
+ flags.force = true;
43
+ } else if (arg === '--config') {
44
+ flags.config = argv[++i];
45
+ } else if (arg === '--delay') {
46
+ flags.delay = parseTime(argv[++i]);
47
+ } else if (arg === '--wait') {
48
+ flags.wait = true;
49
+ } else if (arg === '--timeout') {
50
+ flags.timeout = parseTime(argv[++i]);
51
+ } else if (arg === '--no-preamble') {
52
+ flags.noPreamble = true;
53
+ } else if (arg.startsWith('--pane=')) {
54
+ // Handled in update command
55
+ positional.push(arg);
56
+ } else if (arg.startsWith('--remark=')) {
57
+ // Handled in update command
58
+ positional.push(arg);
59
+ } else if (arg.startsWith('-')) {
60
+ // Unknown flag, pass through
61
+ positional.push(arg);
62
+ } else {
63
+ positional.push(arg);
64
+ }
65
+ i++;
66
+ }
67
+
68
+ const [command = 'help', ...args] = positional;
69
+ return { command, args, flags };
70
+ }
71
+
72
+ /**
73
+ * Parse time string to seconds.
74
+ * Default unit is seconds (no suffix needed).
75
+ */
76
+ function parseTime(value: string): number {
77
+ if (!value) return 0;
78
+
79
+ const match = value.match(/^(\d+(?:\.\d+)?)(ms|s)?$/i);
80
+ if (!match) {
81
+ console.error(
82
+ `Invalid time format: ${value}. Use number (seconds) or number with ms/s suffix.`
83
+ );
84
+ process.exit(ExitCodes.ERROR);
85
+ }
86
+
87
+ const num = parseFloat(match[1]);
88
+ const unit = (match[2] || 's').toLowerCase();
89
+
90
+ if (unit === 'ms') {
91
+ return num / 1000;
92
+ }
93
+ return num; // seconds
94
+ }
95
+
96
+ // ─────────────────────────────────────────────────────────────
97
+ // Main
98
+ // ─────────────────────────────────────────────────────────────
99
+
100
+ function main(): void {
101
+ const argv = process.argv.slice(2);
102
+ const { command, args, flags } = parseArgs(argv);
103
+
104
+ // Help doesn't need context
105
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
106
+ cmdHelp();
107
+ process.exit(ExitCodes.SUCCESS);
108
+ }
109
+
110
+ if (command === '--version' || command === '-V') {
111
+ import('./version.js').then((m) => console.log(m.VERSION));
112
+ return;
113
+ }
114
+
115
+ // Completion doesn't need context
116
+ if (command === 'completion') {
117
+ cmdCompletion(args[0]);
118
+ process.exit(ExitCodes.SUCCESS);
119
+ }
120
+
121
+ // Create context for all other commands
122
+ const ctx = createContext({ argv, flags });
123
+
124
+ const run = async (): Promise<void> => {
125
+ switch (command) {
126
+ case 'init':
127
+ cmdInit(ctx);
128
+ break;
129
+
130
+ case 'list':
131
+ case 'ls':
132
+ cmdList(ctx);
133
+ break;
134
+
135
+ case 'add':
136
+ if (args.length < 2) {
137
+ ctx.ui.error('Usage: tmux-team add <name> <pane> [remark]');
138
+ ctx.exit(ExitCodes.ERROR);
139
+ }
140
+ cmdAdd(ctx, args[0], args[1], args[2]);
141
+ break;
142
+
143
+ case 'update':
144
+ if (args.length < 1) {
145
+ ctx.ui.error('Usage: tmux-team update <name> --pane <pane> | --remark <remark>');
146
+ ctx.exit(ExitCodes.ERROR);
147
+ }
148
+ {
149
+ const options: { pane?: string; remark?: string } = {};
150
+ for (let i = 1; i < args.length; i++) {
151
+ if (args[i] === '--pane' && args[i + 1]) {
152
+ options.pane = args[++i];
153
+ } else if (args[i] === '--remark' && args[i + 1]) {
154
+ options.remark = args[++i];
155
+ } else if (args[i].startsWith('--pane=')) {
156
+ options.pane = args[i].slice(7);
157
+ } else if (args[i].startsWith('--remark=')) {
158
+ options.remark = args[i].slice(9);
159
+ }
160
+ }
161
+ cmdUpdate(ctx, args[0], options);
162
+ }
163
+ break;
164
+
165
+ case 'remove':
166
+ case 'rm':
167
+ if (args.length < 1) {
168
+ ctx.ui.error('Usage: tmux-team remove <name>');
169
+ ctx.exit(ExitCodes.ERROR);
170
+ }
171
+ cmdRemove(ctx, args[0]);
172
+ break;
173
+
174
+ case 'talk':
175
+ case 'send':
176
+ if (args.length < 2) {
177
+ ctx.ui.error('Usage: tmux-team talk <target> <message>');
178
+ ctx.exit(ExitCodes.ERROR);
179
+ }
180
+ await cmdTalk(ctx, args[0], args[1]);
181
+ break;
182
+
183
+ case 'check':
184
+ case 'read':
185
+ if (args.length < 1) {
186
+ ctx.ui.error('Usage: tmux-team check <target> [lines]');
187
+ ctx.exit(ExitCodes.ERROR);
188
+ }
189
+ cmdCheck(ctx, args[0], args[1] ? parseInt(args[1], 10) : undefined);
190
+ break;
191
+
192
+ case 'pm':
193
+ await cmdPm(ctx, args);
194
+ break;
195
+
196
+ default:
197
+ ctx.ui.error(`Unknown command: ${command}. Run 'tmux-team help' for usage.`);
198
+ ctx.exit(ExitCodes.ERROR);
199
+ }
200
+ };
201
+
202
+ run().catch((err) => {
203
+ if (!flags.json) {
204
+ console.error(err);
205
+ } else {
206
+ console.error(JSON.stringify({ error: String(err?.message ?? err) }));
207
+ }
208
+ process.exit(ExitCodes.ERROR);
209
+ });
210
+ }
211
+
212
+ main();
@@ -0,0 +1,38 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // add command - register a new agent
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import fs from 'fs';
6
+ import type { Context } from '../types.js';
7
+ import { ExitCodes } from '../exits.js';
8
+ import { saveLocalConfig } from '../config.js';
9
+
10
+ export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string): void {
11
+ const { ui, config, paths, flags, exit } = ctx;
12
+
13
+ // Create config file if it doesn't exist
14
+ if (!fs.existsSync(paths.localConfig)) {
15
+ fs.writeFileSync(paths.localConfig, '{}\n');
16
+ if (!flags.json) {
17
+ ui.info(`Created ${paths.localConfig}`);
18
+ }
19
+ }
20
+
21
+ if (config.paneRegistry[name]) {
22
+ ui.error(`Agent '${name}' already exists. Use 'tmux-team update' to modify.`);
23
+ exit(ExitCodes.ERROR);
24
+ }
25
+
26
+ config.paneRegistry[name] = { pane };
27
+ if (remark) {
28
+ config.paneRegistry[name].remark = remark;
29
+ }
30
+
31
+ saveLocalConfig(paths, config.paneRegistry);
32
+
33
+ if (flags.json) {
34
+ ui.json({ added: name, pane, remark });
35
+ } else {
36
+ ui.success(`Added agent '${name}' at pane ${pane}`);
37
+ }
38
+ }
@@ -0,0 +1,34 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // check command - capture output from agent's pane
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context } from '../types.js';
6
+ import { ExitCodes } from '../exits.js';
7
+ import { colors } from '../ui.js';
8
+
9
+ export function cmdCheck(ctx: Context, target: string, lines?: number): void {
10
+ const { ui, config, tmux, flags, exit } = ctx;
11
+
12
+ if (!config.paneRegistry[target]) {
13
+ const available = Object.keys(config.paneRegistry).join(', ');
14
+ ui.error(`Agent '${target}' not found. Available: ${available || 'none'}`);
15
+ exit(ExitCodes.PANE_NOT_FOUND);
16
+ }
17
+
18
+ const pane = config.paneRegistry[target].pane;
19
+ const captureLines = lines ?? config.defaults.captureLines;
20
+
21
+ try {
22
+ const output = tmux.capture(pane, captureLines);
23
+
24
+ if (flags.json) {
25
+ ui.json({ target, pane, lines: captureLines, output });
26
+ } else {
27
+ console.log(colors.cyan(`─── Output from ${target} (${pane}) ───`));
28
+ console.log(output);
29
+ }
30
+ } catch {
31
+ ui.error(`Failed to capture pane ${pane}. Is tmux running?`);
32
+ exit(ExitCodes.ERROR);
33
+ }
34
+ }
@@ -0,0 +1,118 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // completion command - shell completion scripts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { colors } from '../ui.js';
6
+
7
+ const zshCompletion = `#compdef tmux-team
8
+
9
+ _tmux-team() {
10
+ local -a commands agents
11
+
12
+ commands=(
13
+ 'talk:Send message to an agent'
14
+ 'check:Capture output from agent pane'
15
+ 'list:List all configured agents'
16
+ 'add:Add a new agent'
17
+ 'update:Update agent config'
18
+ 'remove:Remove an agent'
19
+ 'init:Create empty tmux-team.json'
20
+ 'completion:Output shell completion script'
21
+ 'help:Show help message'
22
+ )
23
+
24
+ _get_agents() {
25
+ if [[ -f ./tmux-team.json ]]; then
26
+ agents=(\${(f)"$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('./tmux-team.json'))).join('\\\\n'))" 2>/dev/null)"})
27
+ fi
28
+ }
29
+
30
+ if (( CURRENT == 2 )); then
31
+ _describe -t commands 'tmux-team commands' commands
32
+ elif (( CURRENT == 3 )); then
33
+ case \${words[2]} in
34
+ talk|check|update|remove|rm)
35
+ _get_agents
36
+ if [[ -n "$agents" ]]; then
37
+ _describe -t agents 'agents' agents
38
+ fi
39
+ if [[ "\${words[2]}" == "talk" ]]; then
40
+ compadd "all"
41
+ fi
42
+ ;;
43
+ completion)
44
+ compadd "zsh" "bash"
45
+ ;;
46
+ esac
47
+ elif (( CURRENT == 4 )); then
48
+ case \${words[2]} in
49
+ update)
50
+ compadd -- "--pane" "--remark"
51
+ ;;
52
+ talk)
53
+ compadd -- "--delay" "--wait" "--timeout"
54
+ ;;
55
+ esac
56
+ fi
57
+ }
58
+
59
+ _tmux-team "$@"`;
60
+
61
+ const bashCompletion = `_tmux_team() {
62
+ local cur prev commands agents
63
+ COMPREPLY=()
64
+ cur="\${COMP_WORDS[COMP_CWORD]}"
65
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
66
+
67
+ commands="talk check list add update remove init completion help"
68
+
69
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
70
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
71
+ elif [[ \${COMP_CWORD} -eq 2 ]]; then
72
+ case "\${prev}" in
73
+ talk|check|update|remove|rm)
74
+ if [[ -f ./tmux-team.json ]]; then
75
+ agents=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('./tmux-team.json'))).join(' '))" 2>/dev/null)
76
+ fi
77
+ if [[ "\${prev}" == "talk" ]]; then
78
+ agents="\${agents} all"
79
+ fi
80
+ COMPREPLY=( $(compgen -W "\${agents}" -- \${cur}) )
81
+ ;;
82
+ completion)
83
+ COMPREPLY=( $(compgen -W "zsh bash" -- \${cur}) )
84
+ ;;
85
+ esac
86
+ elif [[ \${COMP_CWORD} -eq 3 ]]; then
87
+ case "\${COMP_WORDS[1]}" in
88
+ update)
89
+ COMPREPLY=( $(compgen -W "--pane --remark" -- \${cur}) )
90
+ ;;
91
+ talk)
92
+ COMPREPLY=( $(compgen -W "--delay --wait --timeout" -- \${cur}) )
93
+ ;;
94
+ esac
95
+ fi
96
+ }
97
+
98
+ complete -F _tmux_team tmux-team`;
99
+
100
+ export function cmdCompletion(shell?: string): void {
101
+ if (shell === 'bash') {
102
+ console.log(bashCompletion);
103
+ } else if (shell === 'zsh') {
104
+ console.log(zshCompletion);
105
+ } else {
106
+ console.log(`
107
+ ${colors.cyan('Shell Completion Setup')}
108
+
109
+ ${colors.yellow('Zsh')} (add to ~/.zshrc):
110
+ eval "$(tmux-team completion zsh)"
111
+
112
+ ${colors.yellow('Bash')} (add to ~/.bashrc):
113
+ eval "$(tmux-team completion bash)"
114
+
115
+ Then restart your shell or run: source ~/.zshrc (or ~/.bashrc)
116
+ `);
117
+ }
118
+ }
@@ -0,0 +1,51 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // help command - show usage information
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { colors } from '../ui.js';
6
+ import { VERSION } from '../version.js';
7
+
8
+ export function cmdHelp(): void {
9
+ console.log(`
10
+ ${colors.cyan('tmux-team')} v${VERSION} - AI agent collaboration in tmux
11
+
12
+ ${colors.yellow('USAGE')}
13
+ tmux-team <command> [arguments]
14
+
15
+ ${colors.yellow('COMMANDS')}
16
+ ${colors.green('talk')} <target> <message> Send message to an agent (or "all")
17
+ ${colors.green('check')} <target> [lines] Capture output from agent's pane
18
+ ${colors.green('list')} List all configured agents
19
+ ${colors.green('add')} <name> <pane> [remark] Add a new agent
20
+ ${colors.green('update')} <name> [options] Update an agent's config
21
+ ${colors.green('remove')} <name> Remove an agent
22
+ ${colors.green('init')} Create empty tmux-team.json
23
+ ${colors.green('pm')} <subcommand> Project management (run 'pm help')
24
+ ${colors.green('completion')} Output shell completion script
25
+ ${colors.green('help')} Show this help message
26
+
27
+ ${colors.yellow('OPTIONS')}
28
+ ${colors.green('--json')} Output in JSON format
29
+ ${colors.green('--verbose')} Show detailed output
30
+ ${colors.green('--force')} Skip warnings
31
+
32
+ ${colors.yellow('TALK OPTIONS')} ${colors.dim('(v2)')}
33
+ ${colors.green('--delay')} <seconds> Wait before sending (default: seconds)
34
+ ${colors.green('--wait')} Wait for agent response (nonce-based)
35
+ ${colors.green('--timeout')} <seconds> Max wait time (default: 60)
36
+ ${colors.green('--no-preamble')} Skip agent preamble for this message
37
+
38
+ ${colors.yellow('EXAMPLES')}
39
+ tmux-team talk codex "Please review the PR"
40
+ tmux-team talk all "Sync meeting in 5 minutes"
41
+ tmux-team check gemini 50
42
+ tmux-team list --json
43
+ tmux-team add codex 10.1 "Code review specialist"
44
+ tmux-team update codex --pane 10.2
45
+ tmux-team remove codex
46
+
47
+ ${colors.yellow('CONFIG')}
48
+ Local: ./tmux-team.json (pane registry)
49
+ Global: ~/.config/tmux-team/config.json (settings)
50
+ `);
51
+ }
@@ -0,0 +1,24 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // init command - create tmux-team.json
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import fs from 'fs';
6
+ import type { Context } from '../types.js';
7
+ import { ExitCodes } from '../exits.js';
8
+
9
+ export function cmdInit(ctx: Context): void {
10
+ const { ui, paths, flags, exit } = ctx;
11
+
12
+ if (fs.existsSync(paths.localConfig)) {
13
+ ui.error(`${paths.localConfig} already exists. Remove it first if you want to reinitialize.`);
14
+ exit(ExitCodes.ERROR);
15
+ }
16
+
17
+ fs.writeFileSync(paths.localConfig, '{}\n');
18
+
19
+ if (flags.json) {
20
+ ui.json({ created: paths.localConfig });
21
+ } else {
22
+ ui.success(`Created ${paths.localConfig}`);
23
+ }
24
+ }
@@ -0,0 +1,27 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // list command - show configured agents
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context } from '../types.js';
6
+
7
+ export function cmdList(ctx: Context): void {
8
+ const { ui, config, flags } = ctx;
9
+ const agents = Object.entries(config.paneRegistry);
10
+
11
+ if (flags.json) {
12
+ ui.json(config.paneRegistry);
13
+ return;
14
+ }
15
+
16
+ if (agents.length === 0) {
17
+ ui.info("No agents configured. Use 'tmux-team add <name> <pane>' to add one.");
18
+ return;
19
+ }
20
+
21
+ console.log();
22
+ ui.table(
23
+ ['NAME', 'PANE', 'REMARK'],
24
+ agents.map(([name, data]) => [name, data.pane, data.remark || '-'])
25
+ );
26
+ console.log();
27
+ }
@@ -0,0 +1,25 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // remove command - unregister an agent
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context } from '../types.js';
6
+ import { ExitCodes } from '../exits.js';
7
+ import { saveLocalConfig } from '../config.js';
8
+
9
+ export function cmdRemove(ctx: Context, name: string): void {
10
+ const { ui, config, paths, flags, exit } = ctx;
11
+
12
+ if (!config.paneRegistry[name]) {
13
+ ui.error(`Agent '${name}' not found.`);
14
+ exit(ExitCodes.PANE_NOT_FOUND);
15
+ }
16
+
17
+ delete config.paneRegistry[name];
18
+ saveLocalConfig(paths, config.paneRegistry);
19
+
20
+ if (flags.json) {
21
+ ui.json({ removed: name });
22
+ } else {
23
+ ui.success(`Removed agent '${name}'`);
24
+ }
25
+ }