tmux-team 2.0.0-alpha.1 → 2.0.0-alpha.4
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/package.json +4 -2
- package/src/cli.ts +25 -3
- package/src/commands/config.ts +186 -0
- package/src/commands/help.ts +44 -12
- package/src/commands/preamble.ts +153 -0
- package/src/commands/talk.test.ts +160 -5
- package/src/commands/talk.ts +359 -22
- package/src/config.test.ts +1 -1
- package/src/config.ts +70 -6
- package/src/pm/commands.test.ts +1061 -91
- package/src/pm/commands.ts +77 -8
- package/src/pm/manager.ts +12 -6
- package/src/pm/permissions.test.ts +332 -0
- package/src/pm/permissions.ts +279 -0
- package/src/pm/storage/fs.ts +3 -2
- package/src/pm/storage/github.ts +47 -35
- package/src/pm/types.ts +6 -0
- package/src/types.ts +11 -0
- package/src/ui.ts +13 -4
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-team",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.4",
|
|
4
4
|
"description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tmux-team": "./bin/tmux-team"
|
|
7
|
+
"tmux-team": "./bin/tmux-team",
|
|
8
|
+
"tmt": "./bin/tmux-team"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"dev": "tsx src/cli.ts",
|
|
12
|
+
"tmt": "./bin/tmux-team",
|
|
11
13
|
"test": "vitest",
|
|
12
14
|
"test:run": "vitest run",
|
|
13
15
|
"lint": "oxlint src/",
|
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { createContext, ExitCodes } from './context.js';
|
|
|
7
7
|
import type { Flags } from './types.js';
|
|
8
8
|
|
|
9
9
|
// Commands
|
|
10
|
-
import { cmdHelp } from './commands/help.js';
|
|
10
|
+
import { cmdHelp, HelpConfig } from './commands/help.js';
|
|
11
|
+
import { loadConfig, resolvePaths } from './config.js';
|
|
11
12
|
import { cmdInit } from './commands/init.js';
|
|
12
13
|
import { cmdList } from './commands/list.js';
|
|
13
14
|
import { cmdAdd } from './commands/add.js';
|
|
@@ -17,6 +18,8 @@ import { cmdTalk } from './commands/talk.js';
|
|
|
17
18
|
import { cmdCheck } from './commands/check.js';
|
|
18
19
|
import { cmdCompletion } from './commands/completion.js';
|
|
19
20
|
import { cmdPm } from './pm/commands.js';
|
|
21
|
+
import { cmdConfig } from './commands/config.js';
|
|
22
|
+
import { cmdPreamble } from './commands/preamble.js';
|
|
20
23
|
|
|
21
24
|
// ─────────────────────────────────────────────────────────────
|
|
22
25
|
// Argument parsing
|
|
@@ -101,9 +104,20 @@ function main(): void {
|
|
|
101
104
|
const argv = process.argv.slice(2);
|
|
102
105
|
const { command, args, flags } = parseArgs(argv);
|
|
103
106
|
|
|
104
|
-
// Help
|
|
107
|
+
// Help - load config to show current mode/timeout
|
|
105
108
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
106
|
-
|
|
109
|
+
try {
|
|
110
|
+
const paths = resolvePaths();
|
|
111
|
+
const config = loadConfig(paths);
|
|
112
|
+
const helpConfig: HelpConfig = {
|
|
113
|
+
mode: config.mode,
|
|
114
|
+
timeout: config.defaults.timeout,
|
|
115
|
+
};
|
|
116
|
+
cmdHelp(helpConfig);
|
|
117
|
+
} catch {
|
|
118
|
+
// Fallback if config can't be loaded
|
|
119
|
+
cmdHelp();
|
|
120
|
+
}
|
|
107
121
|
process.exit(ExitCodes.SUCCESS);
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -193,6 +207,14 @@ function main(): void {
|
|
|
193
207
|
await cmdPm(ctx, args);
|
|
194
208
|
break;
|
|
195
209
|
|
|
210
|
+
case 'config':
|
|
211
|
+
cmdConfig(ctx, args);
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
case 'preamble':
|
|
215
|
+
cmdPreamble(ctx, args);
|
|
216
|
+
break;
|
|
217
|
+
|
|
196
218
|
default:
|
|
197
219
|
ctx.ui.error(`Unknown command: ${command}. Run 'tmux-team help' for usage.`);
|
|
198
220
|
ctx.exit(ExitCodes.ERROR);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Config command - view and modify settings
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { Context } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../context.js';
|
|
7
|
+
import {
|
|
8
|
+
loadGlobalConfig,
|
|
9
|
+
saveGlobalConfig,
|
|
10
|
+
loadLocalConfigFile,
|
|
11
|
+
saveLocalConfigFile,
|
|
12
|
+
updateLocalSettings,
|
|
13
|
+
clearLocalSettings,
|
|
14
|
+
} from '../config.js';
|
|
15
|
+
|
|
16
|
+
type ConfigKey = 'mode' | 'preambleMode';
|
|
17
|
+
|
|
18
|
+
const VALID_KEYS: ConfigKey[] = ['mode', 'preambleMode'];
|
|
19
|
+
|
|
20
|
+
const VALID_VALUES: Record<ConfigKey, string[]> = {
|
|
21
|
+
mode: ['polling', 'wait'],
|
|
22
|
+
preambleMode: ['always', 'disabled'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isValidKey(key: string): key is ConfigKey {
|
|
26
|
+
return VALID_KEYS.includes(key as ConfigKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isValidValue(key: ConfigKey, value: string): boolean {
|
|
30
|
+
return VALID_VALUES[key].includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show resolved config with source indicators.
|
|
35
|
+
*/
|
|
36
|
+
function showConfig(ctx: Context): void {
|
|
37
|
+
const globalConfig = loadGlobalConfig(ctx.paths);
|
|
38
|
+
const localConfigFile = loadLocalConfigFile(ctx.paths);
|
|
39
|
+
const localSettings = localConfigFile.$config;
|
|
40
|
+
|
|
41
|
+
if (ctx.flags.json) {
|
|
42
|
+
ctx.ui.json({
|
|
43
|
+
resolved: {
|
|
44
|
+
mode: ctx.config.mode,
|
|
45
|
+
preambleMode: ctx.config.preambleMode,
|
|
46
|
+
defaults: ctx.config.defaults,
|
|
47
|
+
},
|
|
48
|
+
sources: {
|
|
49
|
+
mode: localSettings?.mode ? 'local' : globalConfig.mode ? 'global' : 'default',
|
|
50
|
+
preambleMode: localSettings?.preambleMode
|
|
51
|
+
? 'local'
|
|
52
|
+
: globalConfig.preambleMode
|
|
53
|
+
? 'global'
|
|
54
|
+
: 'default',
|
|
55
|
+
},
|
|
56
|
+
paths: {
|
|
57
|
+
global: ctx.paths.globalConfig,
|
|
58
|
+
local: ctx.paths.localConfig,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Determine sources
|
|
65
|
+
const modeSource = localSettings?.mode ? '(local)' : globalConfig.mode ? '(global)' : '(default)';
|
|
66
|
+
const preambleSource = localSettings?.preambleMode
|
|
67
|
+
? '(local)'
|
|
68
|
+
: globalConfig.preambleMode
|
|
69
|
+
? '(global)'
|
|
70
|
+
: '(default)';
|
|
71
|
+
|
|
72
|
+
ctx.ui.info('Current configuration:\n');
|
|
73
|
+
ctx.ui.table(
|
|
74
|
+
['Key', 'Value', 'Source'],
|
|
75
|
+
[
|
|
76
|
+
['mode', ctx.config.mode, modeSource],
|
|
77
|
+
['preambleMode', ctx.config.preambleMode, preambleSource],
|
|
78
|
+
['defaults.timeout', String(ctx.config.defaults.timeout), '(global)'],
|
|
79
|
+
['defaults.pollInterval', String(ctx.config.defaults.pollInterval), '(global)'],
|
|
80
|
+
['defaults.captureLines', String(ctx.config.defaults.captureLines), '(global)'],
|
|
81
|
+
]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
ctx.ui.info(`\nPaths:`);
|
|
85
|
+
ctx.ui.info(` Global: ${ctx.paths.globalConfig}`);
|
|
86
|
+
ctx.ui.info(` Local: ${ctx.paths.localConfig}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set a config value.
|
|
91
|
+
*/
|
|
92
|
+
function setConfig(ctx: Context, key: string, value: string, global: boolean): void {
|
|
93
|
+
if (!isValidKey(key)) {
|
|
94
|
+
ctx.ui.error(`Invalid key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`);
|
|
95
|
+
ctx.exit(ExitCodes.ERROR);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!isValidValue(key, value)) {
|
|
99
|
+
ctx.ui.error(
|
|
100
|
+
`Invalid value for ${key}: ${value}. Valid values: ${VALID_VALUES[key].join(', ')}`
|
|
101
|
+
);
|
|
102
|
+
ctx.exit(ExitCodes.ERROR);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (global) {
|
|
106
|
+
// Set in global config
|
|
107
|
+
const globalConfig = loadGlobalConfig(ctx.paths);
|
|
108
|
+
if (key === 'mode') {
|
|
109
|
+
globalConfig.mode = value as 'polling' | 'wait';
|
|
110
|
+
} else if (key === 'preambleMode') {
|
|
111
|
+
globalConfig.preambleMode = value as 'always' | 'disabled';
|
|
112
|
+
}
|
|
113
|
+
saveGlobalConfig(ctx.paths, globalConfig);
|
|
114
|
+
ctx.ui.success(`Set ${key}=${value} in global config`);
|
|
115
|
+
} else {
|
|
116
|
+
// Set in local config
|
|
117
|
+
if (key === 'mode') {
|
|
118
|
+
updateLocalSettings(ctx.paths, { mode: value as 'polling' | 'wait' });
|
|
119
|
+
} else if (key === 'preambleMode') {
|
|
120
|
+
updateLocalSettings(ctx.paths, { preambleMode: value as 'always' | 'disabled' });
|
|
121
|
+
}
|
|
122
|
+
ctx.ui.success(`Set ${key}=${value} in local config (repo override)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clear local config override.
|
|
128
|
+
*/
|
|
129
|
+
function clearConfig(ctx: Context, key?: string): void {
|
|
130
|
+
if (key) {
|
|
131
|
+
if (!isValidKey(key)) {
|
|
132
|
+
ctx.ui.error(`Invalid key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`);
|
|
133
|
+
ctx.exit(ExitCodes.ERROR);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clear specific key from local settings
|
|
137
|
+
const localConfigFile = loadLocalConfigFile(ctx.paths);
|
|
138
|
+
if (localConfigFile.$config) {
|
|
139
|
+
delete localConfigFile.$config[key];
|
|
140
|
+
// Remove $config if empty
|
|
141
|
+
if (Object.keys(localConfigFile.$config).length === 0) {
|
|
142
|
+
delete localConfigFile.$config;
|
|
143
|
+
}
|
|
144
|
+
saveLocalConfigFile(ctx.paths, localConfigFile);
|
|
145
|
+
}
|
|
146
|
+
ctx.ui.success(`Cleared local override for ${key}`);
|
|
147
|
+
} else {
|
|
148
|
+
// Clear all local settings
|
|
149
|
+
clearLocalSettings(ctx.paths);
|
|
150
|
+
ctx.ui.success('Cleared all local config overrides');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Config command entry point.
|
|
156
|
+
*/
|
|
157
|
+
export function cmdConfig(ctx: Context, args: string[]): void {
|
|
158
|
+
// Parse --global flag first, then determine subcommand
|
|
159
|
+
const globalFlag = args.includes('--global') || args.includes('-g');
|
|
160
|
+
const filteredArgs = args.filter((a) => a !== '--global' && a !== '-g');
|
|
161
|
+
const subcommand = filteredArgs[0];
|
|
162
|
+
|
|
163
|
+
switch (subcommand) {
|
|
164
|
+
case undefined:
|
|
165
|
+
case 'show':
|
|
166
|
+
showConfig(ctx);
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'set':
|
|
170
|
+
if (filteredArgs.length < 3) {
|
|
171
|
+
ctx.ui.error('Usage: tmux-team config set <key> <value> [--global]');
|
|
172
|
+
ctx.exit(ExitCodes.ERROR);
|
|
173
|
+
}
|
|
174
|
+
setConfig(ctx, filteredArgs[1], filteredArgs[2], globalFlag);
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case 'clear':
|
|
178
|
+
clearConfig(ctx, filteredArgs[1]);
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
default:
|
|
182
|
+
ctx.ui.error(`Unknown config subcommand: ${subcommand}`);
|
|
183
|
+
ctx.ui.error('Usage: tmux-team config [show|set|clear]');
|
|
184
|
+
ctx.exit(ExitCodes.ERROR);
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -5,10 +5,30 @@
|
|
|
5
5
|
import { colors } from '../ui.js';
|
|
6
6
|
import { VERSION } from '../version.js';
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export interface HelpConfig {
|
|
9
|
+
mode?: 'polling' | 'wait';
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cmdHelp(config?: HelpConfig): void {
|
|
14
|
+
const mode = config?.mode ?? 'polling';
|
|
15
|
+
const timeout = config?.timeout ?? 180;
|
|
16
|
+
const isWaitMode = mode === 'wait';
|
|
17
|
+
|
|
18
|
+
// Mode indicator with clear explanation
|
|
19
|
+
const modeInfo = isWaitMode
|
|
20
|
+
? `${colors.yellow('CURRENT MODE')}: ${colors.green('wait')} (timeout: ${timeout}s)
|
|
21
|
+
${colors.dim('→ talk commands will BLOCK until agent responds or timeout')}
|
|
22
|
+
${colors.dim('→ Response is returned directly, no need to use check command')}`
|
|
23
|
+
: `${colors.yellow('CURRENT MODE')}: ${colors.cyan('polling')}
|
|
24
|
+
${colors.dim('→ talk commands send and return immediately')}
|
|
25
|
+
${colors.dim('→ Use check command to read agent response')}`;
|
|
26
|
+
|
|
9
27
|
console.log(`
|
|
10
28
|
${colors.cyan('tmux-team')} v${VERSION} - AI agent collaboration in tmux
|
|
11
29
|
|
|
30
|
+
${modeInfo}
|
|
31
|
+
|
|
12
32
|
${colors.yellow('USAGE')}
|
|
13
33
|
tmux-team <command> [arguments]
|
|
14
34
|
|
|
@@ -20,6 +40,8 @@ ${colors.yellow('COMMANDS')}
|
|
|
20
40
|
${colors.green('update')} <name> [options] Update an agent's config
|
|
21
41
|
${colors.green('remove')} <name> Remove an agent
|
|
22
42
|
${colors.green('init')} Create empty tmux-team.json
|
|
43
|
+
${colors.green('config')} [show|set|clear] View/modify settings
|
|
44
|
+
${colors.green('preamble')} [show|set|clear] Manage agent preambles
|
|
23
45
|
${colors.green('pm')} <subcommand> Project management (run 'pm help')
|
|
24
46
|
${colors.green('completion')} Output shell completion script
|
|
25
47
|
${colors.green('help')} Show this help message
|
|
@@ -29,23 +51,33 @@ ${colors.yellow('OPTIONS')}
|
|
|
29
51
|
${colors.green('--verbose')} Show detailed output
|
|
30
52
|
${colors.green('--force')} Skip warnings
|
|
31
53
|
|
|
32
|
-
${colors.yellow('TALK OPTIONS')}
|
|
33
|
-
${colors.green('--delay')} <seconds> Wait before sending
|
|
34
|
-
${colors.green('--wait')}
|
|
35
|
-
${colors.green('--timeout')} <seconds> Max wait time (
|
|
54
|
+
${colors.yellow('TALK OPTIONS')}
|
|
55
|
+
${colors.green('--delay')} <seconds> Wait before sending
|
|
56
|
+
${colors.green('--wait')} Force wait mode (block until response)
|
|
57
|
+
${colors.green('--timeout')} <seconds> Max wait time (current: ${timeout}s)
|
|
36
58
|
${colors.green('--no-preamble')} Skip agent preamble for this message
|
|
37
59
|
|
|
38
|
-
${colors.yellow('EXAMPLES')}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
${colors.yellow('EXAMPLES')}${
|
|
61
|
+
isWaitMode
|
|
62
|
+
? `
|
|
63
|
+
${colors.dim('# Wait mode: commands block until response')}
|
|
64
|
+
tmux-team talk codex "Review this PR" ${colors.dim('← blocks, returns response')}
|
|
65
|
+
tmux-team talk all "Status update" ${colors.dim('← waits for all agents')}`
|
|
66
|
+
: `
|
|
67
|
+
${colors.dim('# Polling mode: send then check')}
|
|
68
|
+
tmux-team talk codex "Review this PR" ${colors.dim('← sends immediately')}
|
|
69
|
+
tmux-team check codex ${colors.dim('← read response later')}`
|
|
70
|
+
}
|
|
42
71
|
tmux-team list --json
|
|
43
72
|
tmux-team add codex 10.1 "Code review specialist"
|
|
44
|
-
tmux-team update codex --pane 10.2
|
|
45
|
-
tmux-team remove codex
|
|
46
73
|
|
|
47
74
|
${colors.yellow('CONFIG')}
|
|
48
|
-
Local: ./tmux-team.json (pane registry)
|
|
75
|
+
Local: ./tmux-team.json (pane registry + $config override)
|
|
49
76
|
Global: ~/.config/tmux-team/config.json (settings)
|
|
77
|
+
|
|
78
|
+
${colors.yellow('CHANGE MODE')}
|
|
79
|
+
tmux-team config set mode wait ${colors.dim('Enable wait mode (local)')}
|
|
80
|
+
tmux-team config set mode polling ${colors.dim('Enable polling mode (local)')}
|
|
81
|
+
tmux-team config set timeout 120 ${colors.dim('Set timeout to 120s')}
|
|
50
82
|
`);
|
|
51
83
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Preamble command - manage agent preambles
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { Context } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../context.js';
|
|
7
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../config.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Show preamble(s) for agent(s).
|
|
11
|
+
*/
|
|
12
|
+
function showPreamble(ctx: Context, agentName?: string): void {
|
|
13
|
+
const { ui, config, flags } = ctx;
|
|
14
|
+
|
|
15
|
+
if (agentName) {
|
|
16
|
+
// Show specific agent's preamble
|
|
17
|
+
const agentConfig = config.agents[agentName];
|
|
18
|
+
const preamble = agentConfig?.preamble;
|
|
19
|
+
|
|
20
|
+
if (flags.json) {
|
|
21
|
+
ui.json({ agent: agentName, preamble: preamble ?? null });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (preamble) {
|
|
26
|
+
ui.info(`Preamble for ${agentName}:`);
|
|
27
|
+
console.log(preamble);
|
|
28
|
+
} else {
|
|
29
|
+
ui.info(`No preamble set for ${agentName}`);
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// Show all preambles
|
|
33
|
+
const preambles: { agent: string; preamble: string }[] = [];
|
|
34
|
+
|
|
35
|
+
for (const [name, agentConfig] of Object.entries(config.agents)) {
|
|
36
|
+
if (agentConfig.preamble) {
|
|
37
|
+
preambles.push({ agent: name, preamble: agentConfig.preamble });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (flags.json) {
|
|
42
|
+
ui.json({ preambles });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (preambles.length === 0) {
|
|
47
|
+
ui.info('No preambles configured');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const { agent, preamble } of preambles) {
|
|
52
|
+
console.log(`─── ${agent} ───`);
|
|
53
|
+
console.log(preamble);
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set preamble for an agent.
|
|
61
|
+
*/
|
|
62
|
+
function setPreamble(ctx: Context, agentName: string, preamble: string): void {
|
|
63
|
+
const { ui, paths, flags } = ctx;
|
|
64
|
+
|
|
65
|
+
const globalConfig = loadGlobalConfig(paths);
|
|
66
|
+
|
|
67
|
+
// Ensure agents object exists
|
|
68
|
+
if (!globalConfig.agents) {
|
|
69
|
+
globalConfig.agents = {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure agent config exists
|
|
73
|
+
if (!globalConfig.agents[agentName]) {
|
|
74
|
+
globalConfig.agents[agentName] = {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
globalConfig.agents[agentName].preamble = preamble;
|
|
78
|
+
saveGlobalConfig(paths, globalConfig);
|
|
79
|
+
|
|
80
|
+
if (flags.json) {
|
|
81
|
+
ui.json({ agent: agentName, preamble, status: 'set' });
|
|
82
|
+
} else {
|
|
83
|
+
ui.success(`Set preamble for ${agentName}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear preamble for an agent.
|
|
89
|
+
*/
|
|
90
|
+
function clearPreamble(ctx: Context, agentName: string): void {
|
|
91
|
+
const { ui, paths, flags } = ctx;
|
|
92
|
+
|
|
93
|
+
const globalConfig = loadGlobalConfig(paths);
|
|
94
|
+
|
|
95
|
+
if (globalConfig.agents?.[agentName]?.preamble) {
|
|
96
|
+
delete globalConfig.agents[agentName].preamble;
|
|
97
|
+
|
|
98
|
+
// Clean up empty agent config
|
|
99
|
+
if (Object.keys(globalConfig.agents[agentName]).length === 0) {
|
|
100
|
+
delete globalConfig.agents[agentName];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
saveGlobalConfig(paths, globalConfig);
|
|
104
|
+
|
|
105
|
+
if (flags.json) {
|
|
106
|
+
ui.json({ agent: agentName, status: 'cleared' });
|
|
107
|
+
} else {
|
|
108
|
+
ui.success(`Cleared preamble for ${agentName}`);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
if (flags.json) {
|
|
112
|
+
ui.json({ agent: agentName, status: 'not_set' });
|
|
113
|
+
} else {
|
|
114
|
+
ui.info(`No preamble was set for ${agentName}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Preamble command entry point.
|
|
121
|
+
*/
|
|
122
|
+
export function cmdPreamble(ctx: Context, args: string[]): void {
|
|
123
|
+
const subcommand = args[0];
|
|
124
|
+
|
|
125
|
+
switch (subcommand) {
|
|
126
|
+
case undefined:
|
|
127
|
+
case 'show':
|
|
128
|
+
showPreamble(ctx, args[1]);
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'set':
|
|
132
|
+
if (args.length < 3) {
|
|
133
|
+
ctx.ui.error('Usage: tmux-team preamble set <agent> <preamble>');
|
|
134
|
+
ctx.exit(ExitCodes.ERROR);
|
|
135
|
+
}
|
|
136
|
+
// Join remaining args as preamble (allows spaces without quotes)
|
|
137
|
+
setPreamble(ctx, args[1], args.slice(2).join(' '));
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'clear':
|
|
141
|
+
if (args.length < 2) {
|
|
142
|
+
ctx.ui.error('Usage: tmux-team preamble clear <agent>');
|
|
143
|
+
ctx.exit(ExitCodes.ERROR);
|
|
144
|
+
}
|
|
145
|
+
clearPreamble(ctx, args[1]);
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
default:
|
|
149
|
+
ctx.ui.error(`Unknown preamble subcommand: ${subcommand}`);
|
|
150
|
+
ctx.ui.error('Usage: tmux-team preamble [show|set|clear]');
|
|
151
|
+
ctx.exit(ExitCodes.ERROR);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -203,12 +203,18 @@ describe('buildMessage (via cmdTalk)', () => {
|
|
|
203
203
|
|
|
204
204
|
describe('cmdTalk - basic send', () => {
|
|
205
205
|
let testDir: string;
|
|
206
|
+
const originalEnv = { ...process.env };
|
|
206
207
|
|
|
207
208
|
beforeEach(() => {
|
|
209
|
+
// Disable pane detection in tests
|
|
210
|
+
delete process.env.TMUX;
|
|
211
|
+
delete process.env.TMT_AGENT_NAME;
|
|
212
|
+
delete process.env.TMUX_TEAM_ACTOR;
|
|
208
213
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
|
|
209
214
|
});
|
|
210
215
|
|
|
211
216
|
afterEach(() => {
|
|
217
|
+
process.env = { ...originalEnv };
|
|
212
218
|
if (fs.existsSync(testDir)) {
|
|
213
219
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
214
220
|
}
|
|
@@ -235,6 +241,27 @@ describe('cmdTalk - basic send', () => {
|
|
|
235
241
|
expect(tmux.sends.map((s) => s.pane).sort()).toEqual(['1.0', '1.1', '1.2']);
|
|
236
242
|
});
|
|
237
243
|
|
|
244
|
+
it('skips self when sending to all (via env var)', async () => {
|
|
245
|
+
// Simulate being an agent via env var (when not in tmux)
|
|
246
|
+
const originalEnv = { ...process.env };
|
|
247
|
+
delete process.env.TMUX; // Ensure pane detection is disabled
|
|
248
|
+
process.env.TMT_AGENT_NAME = 'claude';
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const tmux = createMockTmux();
|
|
252
|
+
const ui = createMockUI();
|
|
253
|
+
const ctx = createContext({ tmux, ui, paths: createTestPaths(testDir) });
|
|
254
|
+
|
|
255
|
+
await cmdTalk(ctx, 'all', 'Hello team');
|
|
256
|
+
|
|
257
|
+
// Should skip claude (self) and only send to codex and gemini
|
|
258
|
+
expect(tmux.sends).toHaveLength(2);
|
|
259
|
+
expect(tmux.sends.map((s) => s.pane).sort()).toEqual(['1.1', '1.2']);
|
|
260
|
+
} finally {
|
|
261
|
+
process.env = originalEnv;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
238
265
|
it('removes exclamation marks for gemini agent', async () => {
|
|
239
266
|
const tmux = createMockTmux();
|
|
240
267
|
const ctx = createContext({ tmux, paths: createTestPaths(testDir) });
|
|
@@ -506,16 +533,144 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
506
533
|
}
|
|
507
534
|
});
|
|
508
535
|
|
|
509
|
-
it('
|
|
536
|
+
it('supports wait mode with all target (parallel polling)', async () => {
|
|
537
|
+
// Create mock tmux that returns markers for each agent after a delay
|
|
538
|
+
const tmux = createMockTmux();
|
|
539
|
+
let captureCount = 0;
|
|
540
|
+
const markersByPane: Record<string, string> = {};
|
|
541
|
+
|
|
542
|
+
// Mock send to capture the marker for each pane
|
|
543
|
+
tmux.send = (pane: string, msg: string) => {
|
|
544
|
+
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
545
|
+
if (match) {
|
|
546
|
+
markersByPane[pane] = match[0];
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Mock capture to return the marker after first poll
|
|
551
|
+
tmux.capture = (pane: string) => {
|
|
552
|
+
captureCount++;
|
|
553
|
+
// Return marker on second capture for each pane
|
|
554
|
+
if (captureCount > 3 && markersByPane[pane]) {
|
|
555
|
+
return `Response from agent\n${markersByPane[pane]}`;
|
|
556
|
+
}
|
|
557
|
+
return 'working...';
|
|
558
|
+
};
|
|
559
|
+
|
|
510
560
|
const ui = createMockUI();
|
|
561
|
+
const paths = createTestPaths(testDir);
|
|
511
562
|
const ctx = createContext({
|
|
512
563
|
ui,
|
|
513
|
-
|
|
514
|
-
|
|
564
|
+
tmux,
|
|
565
|
+
paths,
|
|
566
|
+
flags: { wait: true, timeout: 5 },
|
|
567
|
+
config: {
|
|
568
|
+
defaults: { timeout: 5, pollInterval: 0.05, captureLines: 100 },
|
|
569
|
+
paneRegistry: {
|
|
570
|
+
codex: { pane: '10.1' },
|
|
571
|
+
gemini: { pane: '10.2' },
|
|
572
|
+
},
|
|
573
|
+
},
|
|
515
574
|
});
|
|
516
575
|
|
|
517
|
-
await
|
|
518
|
-
|
|
576
|
+
await cmdTalk(ctx, 'all', 'Hello');
|
|
577
|
+
|
|
578
|
+
// Should have captured both panes
|
|
579
|
+
expect(captureCount).toBeGreaterThan(2);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('handles partial timeout in wait mode with all target', async () => {
|
|
583
|
+
const tmux = createMockTmux();
|
|
584
|
+
const markersByPane: Record<string, string> = {};
|
|
585
|
+
|
|
586
|
+
tmux.send = (pane: string, msg: string) => {
|
|
587
|
+
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
588
|
+
if (match) {
|
|
589
|
+
markersByPane[pane] = match[0];
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// Only pane 10.1 responds, 10.2 times out
|
|
594
|
+
tmux.capture = (pane: string) => {
|
|
595
|
+
if (pane === '10.1' && markersByPane[pane]) {
|
|
596
|
+
return `Response from codex\n${markersByPane[pane]}`;
|
|
597
|
+
}
|
|
598
|
+
return 'still working...';
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const ui = createMockUI();
|
|
602
|
+
const paths = createTestPaths(testDir);
|
|
603
|
+
const ctx = createContext({
|
|
604
|
+
ui,
|
|
605
|
+
tmux,
|
|
606
|
+
paths,
|
|
607
|
+
flags: { wait: true, timeout: 0.1, json: true },
|
|
608
|
+
config: {
|
|
609
|
+
defaults: { timeout: 0.1, pollInterval: 0.02, captureLines: 100 },
|
|
610
|
+
paneRegistry: {
|
|
611
|
+
codex: { pane: '10.1' },
|
|
612
|
+
gemini: { pane: '10.2' },
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
await cmdTalk(ctx, 'all', 'Hello');
|
|
619
|
+
} catch {
|
|
620
|
+
// Expected timeout exit
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Should have JSON output with both results
|
|
624
|
+
expect(ui.jsonOutput.length).toBe(1);
|
|
625
|
+
const result = ui.jsonOutput[0] as {
|
|
626
|
+
summary: { completed: number; timeout: number };
|
|
627
|
+
results: Array<{ agent: string; status: string }>;
|
|
628
|
+
};
|
|
629
|
+
expect(result.summary.completed).toBe(1);
|
|
630
|
+
expect(result.summary.timeout).toBe(1);
|
|
631
|
+
expect(result.results.find((r) => r.agent === 'codex')?.status).toBe('completed');
|
|
632
|
+
expect(result.results.find((r) => r.agent === 'gemini')?.status).toBe('timeout');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('uses unique nonces per agent in broadcast', async () => {
|
|
636
|
+
const tmux = createMockTmux();
|
|
637
|
+
const nonces: string[] = [];
|
|
638
|
+
|
|
639
|
+
tmux.send = (_pane: string, msg: string) => {
|
|
640
|
+
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
641
|
+
if (match) {
|
|
642
|
+
nonces.push(match[1]);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// Return markers immediately
|
|
647
|
+
tmux.capture = (pane: string) => {
|
|
648
|
+
const idx = pane === '10.1' ? 0 : 1;
|
|
649
|
+
if (nonces[idx]) {
|
|
650
|
+
return `Response\n{tmux-team-end:${nonces[idx]}}`;
|
|
651
|
+
}
|
|
652
|
+
return '';
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const paths = createTestPaths(testDir);
|
|
656
|
+
const ctx = createContext({
|
|
657
|
+
tmux,
|
|
658
|
+
paths,
|
|
659
|
+
flags: { wait: true, timeout: 5 },
|
|
660
|
+
config: {
|
|
661
|
+
defaults: { timeout: 5, pollInterval: 0.02, captureLines: 100 },
|
|
662
|
+
paneRegistry: {
|
|
663
|
+
codex: { pane: '10.1' },
|
|
664
|
+
gemini: { pane: '10.2' },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
await cmdTalk(ctx, 'all', 'Hello');
|
|
670
|
+
|
|
671
|
+
// Each agent should have a unique nonce
|
|
672
|
+
expect(nonces.length).toBe(2);
|
|
673
|
+
expect(nonces[0]).not.toBe(nonces[1]);
|
|
519
674
|
});
|
|
520
675
|
});
|
|
521
676
|
|