tmux-team 2.0.0-alpha.1 → 2.0.0-alpha.3
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 +10 -0
- package/src/commands/config.ts +187 -0
- package/src/commands/help.ts +16 -1
- package/src/commands/preamble.ts +153 -0
- package/src/commands/talk.test.ts +27 -0
- package/src/commands/talk.ts +13 -0
- package/src/config.ts +69 -5
- package/src/pm/commands.test.ts +1060 -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 +278 -0
- package/src/pm/storage/fs.ts +3 -2
- package/src/pm/storage/github.ts +45 -33
- 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.3",
|
|
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
|
@@ -17,6 +17,8 @@ import { cmdTalk } from './commands/talk.js';
|
|
|
17
17
|
import { cmdCheck } from './commands/check.js';
|
|
18
18
|
import { cmdCompletion } from './commands/completion.js';
|
|
19
19
|
import { cmdPm } from './pm/commands.js';
|
|
20
|
+
import { cmdConfig } from './commands/config.js';
|
|
21
|
+
import { cmdPreamble } from './commands/preamble.js';
|
|
20
22
|
|
|
21
23
|
// ─────────────────────────────────────────────────────────────
|
|
22
24
|
// Argument parsing
|
|
@@ -193,6 +195,14 @@ function main(): void {
|
|
|
193
195
|
await cmdPm(ctx, args);
|
|
194
196
|
break;
|
|
195
197
|
|
|
198
|
+
case 'config':
|
|
199
|
+
cmdConfig(ctx, args);
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'preamble':
|
|
203
|
+
cmdPreamble(ctx, args);
|
|
204
|
+
break;
|
|
205
|
+
|
|
196
206
|
default:
|
|
197
207
|
ctx.ui.error(`Unknown command: ${command}. Run 'tmux-team help' for usage.`);
|
|
198
208
|
ctx.exit(ExitCodes.ERROR);
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
const subcommand = args[0];
|
|
159
|
+
|
|
160
|
+
// Parse --global flag
|
|
161
|
+
const globalFlag = args.includes('--global') || args.includes('-g');
|
|
162
|
+
const filteredArgs = args.filter((a) => a !== '--global' && a !== '-g');
|
|
163
|
+
|
|
164
|
+
switch (subcommand) {
|
|
165
|
+
case undefined:
|
|
166
|
+
case 'show':
|
|
167
|
+
showConfig(ctx);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'set':
|
|
171
|
+
if (filteredArgs.length < 3) {
|
|
172
|
+
ctx.ui.error('Usage: tmux-team config set <key> <value> [--global]');
|
|
173
|
+
ctx.exit(ExitCodes.ERROR);
|
|
174
|
+
}
|
|
175
|
+
setConfig(ctx, filteredArgs[1], filteredArgs[2], globalFlag);
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'clear':
|
|
179
|
+
clearConfig(ctx, filteredArgs[1]);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
ctx.ui.error(`Unknown config subcommand: ${subcommand}`);
|
|
184
|
+
ctx.ui.error('Usage: tmux-team config [show|set|clear]');
|
|
185
|
+
ctx.exit(ExitCodes.ERROR);
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -20,6 +20,8 @@ ${colors.yellow('COMMANDS')}
|
|
|
20
20
|
${colors.green('update')} <name> [options] Update an agent's config
|
|
21
21
|
${colors.green('remove')} <name> Remove an agent
|
|
22
22
|
${colors.green('init')} Create empty tmux-team.json
|
|
23
|
+
${colors.green('config')} [show|set|clear] View/modify settings
|
|
24
|
+
${colors.green('preamble')} [show|set|clear] Manage agent preambles
|
|
23
25
|
${colors.green('pm')} <subcommand> Project management (run 'pm help')
|
|
24
26
|
${colors.green('completion')} Output shell completion script
|
|
25
27
|
${colors.green('help')} Show this help message
|
|
@@ -45,7 +47,20 @@ ${colors.yellow('EXAMPLES')}
|
|
|
45
47
|
tmux-team remove codex
|
|
46
48
|
|
|
47
49
|
${colors.yellow('CONFIG')}
|
|
48
|
-
Local: ./tmux-team.json (pane registry)
|
|
50
|
+
Local: ./tmux-team.json (pane registry + $config override)
|
|
49
51
|
Global: ~/.config/tmux-team/config.json (settings)
|
|
52
|
+
|
|
53
|
+
${colors.yellow('CONFIG EXAMPLES')}
|
|
54
|
+
tmux-team config Show current settings
|
|
55
|
+
tmux-team config set mode wait Set mode in local config (repo override)
|
|
56
|
+
tmux-team config set mode polling --global Set mode in global config
|
|
57
|
+
tmux-team config clear mode Clear local override for mode
|
|
58
|
+
tmux-team config clear Clear all local overrides
|
|
59
|
+
|
|
60
|
+
${colors.yellow('PREAMBLE EXAMPLES')}
|
|
61
|
+
tmux-team preamble Show all preambles
|
|
62
|
+
tmux-team preamble show codex Show preamble for codex
|
|
63
|
+
tmux-team preamble set codex "You are a code reviewer. Be concise."
|
|
64
|
+
tmux-team preamble clear codex Clear preamble for codex
|
|
50
65
|
`);
|
|
51
66
|
}
|
|
@@ -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) });
|
package/src/commands/talk.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ExitCodes } from '../exits.js';
|
|
|
8
8
|
import { colors } from '../ui.js';
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
import { cleanupState, clearActiveRequest, setActiveRequest } from '../state.js';
|
|
11
|
+
import { resolveActor } from '../pm/permissions.js';
|
|
11
12
|
|
|
12
13
|
function sleepMs(ms: number): Promise<void> {
|
|
13
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -65,6 +66,9 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
65
66
|
exit(ExitCodes.CONFIG_MISSING);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Determine current agent to skip self
|
|
70
|
+
const { actor: self } = resolveActor(config.paneRegistry);
|
|
71
|
+
|
|
68
72
|
if (flags.delay && flags.delay > 0) {
|
|
69
73
|
await sleepMs(flags.delay * 1000);
|
|
70
74
|
}
|
|
@@ -72,6 +76,15 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
72
76
|
const results: { agent: string; pane: string; status: string }[] = [];
|
|
73
77
|
|
|
74
78
|
for (const [name, data] of agents) {
|
|
79
|
+
// Skip sending to self
|
|
80
|
+
if (name === self) {
|
|
81
|
+
results.push({ agent: name, pane: data.pane, status: 'skipped (self)' });
|
|
82
|
+
if (!flags.json) {
|
|
83
|
+
console.log(`${colors.dim('○')} Skipped ${colors.cyan(name)} (self)`);
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
try {
|
|
76
89
|
// Build message with preamble, then apply Gemini filter
|
|
77
90
|
let msg = buildMessage(message, name, ctx);
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
GlobalConfig,
|
|
10
|
+
LocalConfig,
|
|
11
|
+
LocalConfigFile,
|
|
12
|
+
LocalSettings,
|
|
13
|
+
ResolvedConfig,
|
|
14
|
+
Paths,
|
|
15
|
+
} from './types.js';
|
|
9
16
|
|
|
10
17
|
const CONFIG_FILENAME = 'config.json';
|
|
11
18
|
const LOCAL_CONFIG_FILENAME = 'tmux-team.json';
|
|
@@ -136,10 +143,20 @@ export function loadConfig(paths: Paths): ResolvedConfig {
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
// Load local config (pane registry)
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
146
|
+
// Load local config (pane registry + optional settings)
|
|
147
|
+
const localConfigFile = loadJsonFile<LocalConfigFile>(paths.localConfig);
|
|
148
|
+
if (localConfigFile) {
|
|
149
|
+
// Extract local settings if present
|
|
150
|
+
const { $config: localSettings, ...paneEntries } = localConfigFile;
|
|
151
|
+
|
|
152
|
+
// Merge local settings (override global)
|
|
153
|
+
if (localSettings) {
|
|
154
|
+
if (localSettings.mode) config.mode = localSettings.mode;
|
|
155
|
+
if (localSettings.preambleMode) config.preambleMode = localSettings.preambleMode;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Set pane registry (filter out $config)
|
|
159
|
+
config.paneRegistry = paneEntries as LocalConfig;
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
return config;
|
|
@@ -157,3 +174,50 @@ export function ensureGlobalDir(paths: Paths): void {
|
|
|
157
174
|
fs.mkdirSync(paths.globalDir, { recursive: true });
|
|
158
175
|
}
|
|
159
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Load raw global config file (for editing).
|
|
180
|
+
*/
|
|
181
|
+
export function loadGlobalConfig(paths: Paths): Partial<GlobalConfig> {
|
|
182
|
+
return loadJsonFile<Partial<GlobalConfig>>(paths.globalConfig) ?? {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Save global config file.
|
|
187
|
+
*/
|
|
188
|
+
export function saveGlobalConfig(paths: Paths, config: Partial<GlobalConfig>): void {
|
|
189
|
+
ensureGlobalDir(paths);
|
|
190
|
+
fs.writeFileSync(paths.globalConfig, JSON.stringify(config, null, 2) + '\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load raw local config file (for editing).
|
|
195
|
+
*/
|
|
196
|
+
export function loadLocalConfigFile(paths: Paths): LocalConfigFile {
|
|
197
|
+
return loadJsonFile<LocalConfigFile>(paths.localConfig) ?? {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Save local config file (preserves both $config and pane entries).
|
|
202
|
+
*/
|
|
203
|
+
export function saveLocalConfigFile(paths: Paths, configFile: LocalConfigFile): void {
|
|
204
|
+
fs.writeFileSync(paths.localConfig, JSON.stringify(configFile, null, 2) + '\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Update local settings (creates $config if needed).
|
|
209
|
+
*/
|
|
210
|
+
export function updateLocalSettings(paths: Paths, settings: LocalSettings): void {
|
|
211
|
+
const configFile = loadLocalConfigFile(paths);
|
|
212
|
+
configFile.$config = { ...configFile.$config, ...settings };
|
|
213
|
+
saveLocalConfigFile(paths, configFile);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clear local settings.
|
|
218
|
+
*/
|
|
219
|
+
export function clearLocalSettings(paths: Paths): void {
|
|
220
|
+
const configFile = loadLocalConfigFile(paths);
|
|
221
|
+
delete configFile.$config;
|
|
222
|
+
saveLocalConfigFile(paths, configFile);
|
|
223
|
+
}
|