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.
- package/README.md +56 -248
- package/package.json +5 -3
- package/skills/README.md +9 -10
- package/src/cli.test.ts +163 -0
- package/src/cli.ts +29 -18
- package/src/commands/basic-commands.test.ts +252 -0
- package/src/commands/config-command.test.ts +116 -0
- package/src/commands/help.ts +19 -3
- package/src/commands/install.test.ts +205 -0
- package/src/commands/install.ts +207 -0
- package/src/commands/learn.ts +80 -0
- package/src/commands/setup.test.ts +175 -0
- package/src/commands/setup.ts +163 -0
- package/src/commands/talk.test.ts +169 -101
- package/src/commands/talk.ts +186 -98
- package/src/context.test.ts +68 -0
- package/src/identity.test.ts +70 -0
- package/src/state.test.ts +14 -0
- package/src/tmux.test.ts +50 -0
- package/src/tmux.ts +66 -2
- package/src/types.ts +10 -1
- package/src/ui.test.ts +63 -0
- package/src/version.test.ts +31 -0
- package/src/version.ts +1 -1
- package/src/commands/install-skill.ts +0 -148
package/src/tmux.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// Pure tmux wrapper - send-keys, capture-pane
|
|
2
|
+
// Pure tmux wrapper - send-keys, capture-pane, pane detection
|
|
3
3
|
// ─────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
-
import type { Tmux } from './types.js';
|
|
6
|
+
import type { Tmux, PaneInfo } from './types.js';
|
|
7
|
+
|
|
8
|
+
// Known agent patterns for auto-detection
|
|
9
|
+
const KNOWN_AGENTS: Record<string, string[]> = {
|
|
10
|
+
claude: ['claude', 'claude-code'],
|
|
11
|
+
codex: ['codex'],
|
|
12
|
+
gemini: ['gemini'],
|
|
13
|
+
aider: ['aider'],
|
|
14
|
+
cursor: ['cursor'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function detectAgentName(command: string): string | null {
|
|
18
|
+
const lowerCommand = command.toLowerCase();
|
|
19
|
+
for (const [agentName, patterns] of Object.entries(KNOWN_AGENTS)) {
|
|
20
|
+
for (const pattern of patterns) {
|
|
21
|
+
if (lowerCommand.includes(pattern)) {
|
|
22
|
+
return agentName;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
7
28
|
|
|
8
29
|
export function createTmux(): Tmux {
|
|
9
30
|
return {
|
|
@@ -23,5 +44,48 @@ export function createTmux(): Tmux {
|
|
|
23
44
|
});
|
|
24
45
|
return output;
|
|
25
46
|
},
|
|
47
|
+
|
|
48
|
+
listPanes(): PaneInfo[] {
|
|
49
|
+
try {
|
|
50
|
+
// Get all panes with their IDs and current commands
|
|
51
|
+
const output = execSync('tmux list-panes -a -F "#{pane_id}\t#{pane_current_command}"', {
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return output
|
|
57
|
+
.trim()
|
|
58
|
+
.split('\n')
|
|
59
|
+
.filter((line) => line.trim())
|
|
60
|
+
.map((line) => {
|
|
61
|
+
const [id, command] = line.split('\t');
|
|
62
|
+
return {
|
|
63
|
+
id: id || '',
|
|
64
|
+
command: command || '',
|
|
65
|
+
suggestedName: detectAgentName(command || ''),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
getCurrentPaneId(): string | null {
|
|
74
|
+
// First check environment variable
|
|
75
|
+
if (process.env.TMUX_PANE) {
|
|
76
|
+
return process.env.TMUX_PANE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fall back to tmux command
|
|
80
|
+
try {
|
|
81
|
+
const output = execSync('tmux display-message -p "#{pane_id}"', {
|
|
82
|
+
encoding: 'utf-8',
|
|
83
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
+
});
|
|
85
|
+
return output.trim() || null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
26
90
|
};
|
|
27
91
|
}
|
package/src/types.ts
CHANGED
|
@@ -53,11 +53,13 @@ export interface ResolvedConfig {
|
|
|
53
53
|
export interface Flags {
|
|
54
54
|
json: boolean;
|
|
55
55
|
verbose: boolean;
|
|
56
|
+
debug?: boolean;
|
|
56
57
|
config?: string;
|
|
57
58
|
force?: boolean;
|
|
58
59
|
delay?: number; // seconds
|
|
59
60
|
wait?: boolean;
|
|
60
61
|
timeout?: number; // seconds
|
|
62
|
+
lines?: number; // lines to capture before end marker
|
|
61
63
|
noPreamble?: boolean;
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -77,15 +79,22 @@ export interface UI {
|
|
|
77
79
|
json: (data: unknown) => void;
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
export interface PaneInfo {
|
|
83
|
+
id: string; // e.g., "%1"
|
|
84
|
+
command: string; // e.g., "node", "python", "zsh"
|
|
85
|
+
suggestedName: string | null; // e.g., "codex" if detected from command
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
export interface Tmux {
|
|
81
89
|
send: (paneId: string, message: string) => void;
|
|
82
90
|
capture: (paneId: string, lines: number) => string;
|
|
91
|
+
listPanes: () => PaneInfo[];
|
|
92
|
+
getCurrentPaneId: () => string | null;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export interface WaitResult {
|
|
86
96
|
requestId: string;
|
|
87
97
|
nonce: string;
|
|
88
|
-
startMarker: string;
|
|
89
98
|
endMarker: string;
|
|
90
99
|
response: string;
|
|
91
100
|
}
|
package/src/ui.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('ui', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('createUI(jsonMode=true) suppresses human output and emits JSON errors', async () => {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
15
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
16
|
+
|
|
17
|
+
const { createUI } = await import('./ui.js');
|
|
18
|
+
const ui = createUI(true);
|
|
19
|
+
|
|
20
|
+
ui.info('x');
|
|
21
|
+
ui.success('x');
|
|
22
|
+
ui.warn('x');
|
|
23
|
+
ui.table(['A'], [['B']]);
|
|
24
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
25
|
+
|
|
26
|
+
ui.error('boom');
|
|
27
|
+
expect(errSpy).toHaveBeenCalledWith(JSON.stringify({ error: 'boom' }));
|
|
28
|
+
|
|
29
|
+
ui.json({ ok: true });
|
|
30
|
+
expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ ok: true }, null, 2));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('createUI(jsonMode=false) prints messages and table', async () => {
|
|
34
|
+
vi.resetModules();
|
|
35
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
36
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
37
|
+
|
|
38
|
+
const { createUI } = await import('./ui.js');
|
|
39
|
+
const ui = createUI(false);
|
|
40
|
+
|
|
41
|
+
ui.info('hello');
|
|
42
|
+
ui.success('ok');
|
|
43
|
+
ui.warn('warn');
|
|
44
|
+
ui.error('err');
|
|
45
|
+
|
|
46
|
+
expect(logSpy).toHaveBeenCalled();
|
|
47
|
+
expect(errSpy).toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
logSpy.mockClear();
|
|
50
|
+
ui.table(
|
|
51
|
+
['Name', 'Pane'],
|
|
52
|
+
[
|
|
53
|
+
['claude', '1.0'],
|
|
54
|
+
['codex', '1.1'],
|
|
55
|
+
]
|
|
56
|
+
);
|
|
57
|
+
const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
58
|
+
expect(output).toContain('Name');
|
|
59
|
+
expect(output).toContain('Pane');
|
|
60
|
+
expect(output).toContain('claude');
|
|
61
|
+
expect(output).toContain('1.0');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('version', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('reads VERSION from package.json when possible', async () => {
|
|
9
|
+
vi.resetModules();
|
|
10
|
+
const { VERSION } = await import('./version.js');
|
|
11
|
+
expect(typeof VERSION).toBe('string');
|
|
12
|
+
expect(VERSION.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('falls back to hardcoded version when package.json read fails', async () => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
vi.doMock('fs', () => ({
|
|
18
|
+
default: {
|
|
19
|
+
readFileSync: () => {
|
|
20
|
+
throw new Error('read fail');
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
readFileSync: () => {
|
|
24
|
+
throw new Error('read fail');
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { VERSION } = await import('./version.js');
|
|
29
|
+
expect(VERSION).toBe('3.0.1');
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/version.ts
CHANGED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// install-skill command - install tmux-team skills 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 { fileURLToPath } from 'node:url';
|
|
9
|
-
import type { Context } from '../types.js';
|
|
10
|
-
import { ExitCodes } from '../context.js';
|
|
11
|
-
|
|
12
|
-
type AgentType = 'claude' | 'codex';
|
|
13
|
-
type Scope = 'user' | 'local';
|
|
14
|
-
|
|
15
|
-
interface SkillConfig {
|
|
16
|
-
sourceFile: string;
|
|
17
|
-
userDir: string;
|
|
18
|
-
localDir: string;
|
|
19
|
-
targetFile: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getCodexHome(): string {
|
|
23
|
-
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const SKILL_CONFIGS: Record<AgentType, SkillConfig> = {
|
|
27
|
-
claude: {
|
|
28
|
-
sourceFile: 'skills/claude/team.md',
|
|
29
|
-
userDir: path.join(os.homedir(), '.claude', 'commands'),
|
|
30
|
-
localDir: '.claude/commands',
|
|
31
|
-
targetFile: 'team.md',
|
|
32
|
-
},
|
|
33
|
-
codex: {
|
|
34
|
-
sourceFile: 'skills/codex/SKILL.md',
|
|
35
|
-
userDir: path.join(getCodexHome(), 'skills', 'tmux-team'),
|
|
36
|
-
localDir: '.codex/skills/tmux-team',
|
|
37
|
-
targetFile: 'SKILL.md',
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const SUPPORTED_AGENTS = Object.keys(SKILL_CONFIGS) as AgentType[];
|
|
42
|
-
|
|
43
|
-
function findPackageRoot(): string {
|
|
44
|
-
// Get current file's directory (ES modules don't have __dirname)
|
|
45
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
46
|
-
let dir = path.dirname(currentFile);
|
|
47
|
-
|
|
48
|
-
// Try to find the package root by looking for package.json
|
|
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
|
-
// Fallback: assume we're in src/commands
|
|
59
|
-
return path.resolve(path.dirname(currentFile), '..', '..');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function exitWithError(ctx: Context, error: string, hint?: string): never {
|
|
63
|
-
if (ctx.flags.json) {
|
|
64
|
-
ctx.ui.json({ success: false, error, hint });
|
|
65
|
-
} else {
|
|
66
|
-
ctx.ui.error(error);
|
|
67
|
-
if (hint) ctx.ui.info(hint);
|
|
68
|
-
}
|
|
69
|
-
ctx.exit(ExitCodes.ERROR);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function cmdInstallSkill(ctx: Context, agent?: string, scope: string = 'user'): void {
|
|
73
|
-
// Validate agent
|
|
74
|
-
if (!agent) {
|
|
75
|
-
exitWithError(
|
|
76
|
-
ctx,
|
|
77
|
-
'Usage: tmux-team install-skill <agent> [--local|--user]',
|
|
78
|
-
`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const agentLower = agent.toLowerCase() as AgentType;
|
|
83
|
-
if (!SUPPORTED_AGENTS.includes(agentLower)) {
|
|
84
|
-
exitWithError(
|
|
85
|
-
ctx,
|
|
86
|
-
`Unknown agent: ${agent}`,
|
|
87
|
-
`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Validate scope
|
|
92
|
-
const scopeLower = scope.toLowerCase() as Scope;
|
|
93
|
-
if (scopeLower !== 'user' && scopeLower !== 'local') {
|
|
94
|
-
exitWithError(ctx, `Invalid scope: ${scope}. Use 'user' or 'local'.`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const config = SKILL_CONFIGS[agentLower];
|
|
98
|
-
const pkgRoot = findPackageRoot();
|
|
99
|
-
const sourcePath = path.join(pkgRoot, config.sourceFile);
|
|
100
|
-
|
|
101
|
-
// Check source file exists
|
|
102
|
-
if (!fs.existsSync(sourcePath)) {
|
|
103
|
-
exitWithError(
|
|
104
|
-
ctx,
|
|
105
|
-
`Skill file not found: ${sourcePath}`,
|
|
106
|
-
'Make sure tmux-team is properly installed.'
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Determine target directory
|
|
111
|
-
const targetDir = scopeLower === 'user' ? config.userDir : path.resolve(config.localDir);
|
|
112
|
-
const targetPath = path.join(targetDir, config.targetFile);
|
|
113
|
-
|
|
114
|
-
// Check if already exists
|
|
115
|
-
if (fs.existsSync(targetPath) && !ctx.flags.force) {
|
|
116
|
-
exitWithError(ctx, `Skill already exists: ${targetPath}`, 'Use --force to overwrite.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Create directory if needed
|
|
120
|
-
if (!fs.existsSync(targetDir)) {
|
|
121
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
122
|
-
if (ctx.flags.verbose) {
|
|
123
|
-
ctx.ui.info(`Created directory: ${targetDir}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Copy file
|
|
128
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
129
|
-
|
|
130
|
-
if (ctx.flags.json) {
|
|
131
|
-
ctx.ui.json({
|
|
132
|
-
success: true,
|
|
133
|
-
agent: agentLower,
|
|
134
|
-
scope: scopeLower,
|
|
135
|
-
path: targetPath,
|
|
136
|
-
});
|
|
137
|
-
} else {
|
|
138
|
-
ctx.ui.success(`Installed ${agentLower} skill to ${targetPath}`);
|
|
139
|
-
|
|
140
|
-
// Show usage hint
|
|
141
|
-
if (agentLower === 'claude') {
|
|
142
|
-
ctx.ui.info('Usage: /team talk codex "message"');
|
|
143
|
-
} else if (agentLower === 'codex') {
|
|
144
|
-
ctx.ui.info('Enable skills: codex --enable skills');
|
|
145
|
-
ctx.ui.info('Usage: $tmux-team or implicit invocation');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|