tmux-team 2.1.0 → 3.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/README.md +89 -53
- package/package.json +1 -1
- package/src/cli.ts +0 -5
- package/src/commands/add.ts +9 -5
- package/src/commands/config.ts +62 -9
- package/src/commands/help.ts +2 -2
- package/src/commands/preamble.ts +22 -25
- package/src/commands/remove.ts +5 -3
- package/src/commands/talk.test.ts +479 -44
- package/src/commands/talk.ts +95 -65
- package/src/commands/update.ts +16 -5
- package/src/config.test.ts +182 -9
- package/src/config.ts +28 -16
- package/src/identity.ts +89 -0
- package/src/state.test.ts +20 -10
- package/src/state.ts +28 -1
- package/src/types.ts +6 -2
- package/src/ui.ts +2 -1
- package/src/version.ts +1 -1
- package/src/pm/commands.test.ts +0 -1128
- package/src/pm/commands.ts +0 -723
- package/src/pm/manager.test.ts +0 -377
- package/src/pm/manager.ts +0 -146
- package/src/pm/permissions.test.ts +0 -332
- package/src/pm/permissions.ts +0 -279
- package/src/pm/storage/adapter.ts +0 -55
- package/src/pm/storage/fs.test.ts +0 -384
- package/src/pm/storage/fs.ts +0 -256
- package/src/pm/storage/github.ts +0 -763
- package/src/pm/types.ts +0 -85
package/src/identity.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Identity resolution - determine current agent from tmux pane
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import type { PaneEntry } from './types.js';
|
|
7
|
+
|
|
8
|
+
export interface ActorResolution {
|
|
9
|
+
actor: string;
|
|
10
|
+
source: 'pane' | 'env' | 'default';
|
|
11
|
+
warning?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get current tmux pane ID (e.g., "1.0").
|
|
16
|
+
*/
|
|
17
|
+
function getCurrentPane(): string | null {
|
|
18
|
+
if (!process.env.TMUX) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tmuxPane = process.env.TMUX_PANE;
|
|
23
|
+
if (!tmuxPane) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = execSync(
|
|
29
|
+
`tmux display-message -p -t "${tmuxPane}" '#{window_index}.#{pane_index}'`,
|
|
30
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
31
|
+
);
|
|
32
|
+
return result.trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find agent name by pane ID.
|
|
40
|
+
*/
|
|
41
|
+
function findAgentByPane(paneRegistry: Record<string, PaneEntry>, paneId: string): string | null {
|
|
42
|
+
for (const [agentName, entry] of Object.entries(paneRegistry)) {
|
|
43
|
+
if (entry.pane === paneId) {
|
|
44
|
+
return agentName;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve current actor using pane registry as primary source.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveActor(paneRegistry: Record<string, PaneEntry>): ActorResolution {
|
|
54
|
+
const envActor = process.env.TMT_AGENT_NAME || process.env.TMUX_TEAM_ACTOR;
|
|
55
|
+
const currentPane = getCurrentPane();
|
|
56
|
+
|
|
57
|
+
// Not in tmux - use env var or default to human
|
|
58
|
+
if (!currentPane) {
|
|
59
|
+
if (envActor) {
|
|
60
|
+
return { actor: envActor, source: 'env' };
|
|
61
|
+
}
|
|
62
|
+
return { actor: 'human', source: 'default' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// In tmux - look up pane in registry
|
|
66
|
+
const paneAgent = findAgentByPane(paneRegistry, currentPane);
|
|
67
|
+
|
|
68
|
+
if (paneAgent) {
|
|
69
|
+
if (envActor && envActor !== paneAgent) {
|
|
70
|
+
return {
|
|
71
|
+
actor: paneAgent,
|
|
72
|
+
source: 'pane',
|
|
73
|
+
warning: `⚠️ Identity mismatch: TMT_AGENT_NAME="${envActor}" but pane ${currentPane} is registered to "${paneAgent}". Using pane identity.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { actor: paneAgent, source: 'pane' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pane not in registry
|
|
80
|
+
if (envActor) {
|
|
81
|
+
return {
|
|
82
|
+
actor: envActor,
|
|
83
|
+
source: 'env',
|
|
84
|
+
warning: `⚠️ Unregistered pane: pane ${currentPane} is not in registry. Using TMT_AGENT_NAME="${envActor}".`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { actor: 'human', source: 'default' };
|
|
89
|
+
}
|
package/src/state.test.ts
CHANGED
|
@@ -51,8 +51,11 @@ describe('State Management', () => {
|
|
|
51
51
|
fs.writeFileSync(paths.stateFile, JSON.stringify(existingState));
|
|
52
52
|
|
|
53
53
|
const state = loadState(paths);
|
|
54
|
-
expect(state.requests.claude).
|
|
55
|
-
|
|
54
|
+
expect(state.requests.claude).toMatchObject({
|
|
55
|
+
id: 'req-1',
|
|
56
|
+
nonce: 'abc123',
|
|
57
|
+
pane: '1.0',
|
|
58
|
+
});
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
it('returns empty state when state.json is corrupted', () => {
|
|
@@ -123,7 +126,11 @@ describe('State Management', () => {
|
|
|
123
126
|
|
|
124
127
|
const cleaned = cleanupState(paths, 60); // 60 second TTL
|
|
125
128
|
|
|
126
|
-
expect(cleaned.requests.recentAgent).
|
|
129
|
+
expect(cleaned.requests.recentAgent).toMatchObject({
|
|
130
|
+
id: 'new',
|
|
131
|
+
nonce: 'new',
|
|
132
|
+
pane: '1.0',
|
|
133
|
+
});
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
it('requires ttlSeconds parameter', () => {
|
|
@@ -142,7 +149,7 @@ describe('State Management', () => {
|
|
|
142
149
|
// With 10s TTL, agent1 should be kept, agent2 removed
|
|
143
150
|
const cleaned = cleanupState(paths, 10);
|
|
144
151
|
|
|
145
|
-
expect(cleaned.requests.agent1).
|
|
152
|
+
expect(cleaned.requests.agent1).toMatchObject({ id: '1', nonce: 'a' });
|
|
146
153
|
expect(cleaned.requests.agent2).toBeUndefined();
|
|
147
154
|
});
|
|
148
155
|
|
|
@@ -193,8 +200,11 @@ describe('State Management', () => {
|
|
|
193
200
|
setActiveRequest(paths, 'claude', req);
|
|
194
201
|
|
|
195
202
|
const state = loadState(paths);
|
|
196
|
-
expect(state.requests.claude).
|
|
197
|
-
|
|
203
|
+
expect(state.requests.claude).toMatchObject({
|
|
204
|
+
id: 'req-1',
|
|
205
|
+
nonce: 'nonce123',
|
|
206
|
+
pane: '1.0',
|
|
207
|
+
});
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
it('stores request with id, nonce, pane, and startedAtMs', () => {
|
|
@@ -244,8 +254,8 @@ describe('State Management', () => {
|
|
|
244
254
|
setActiveRequest(paths, 'codex', req2);
|
|
245
255
|
|
|
246
256
|
const state = loadState(paths);
|
|
247
|
-
expect(state.requests.claude).
|
|
248
|
-
expect(state.requests.codex).
|
|
257
|
+
expect(state.requests.claude).toMatchObject({ id: '1', nonce: 'a' });
|
|
258
|
+
expect(state.requests.codex).toMatchObject({ id: '2', nonce: 'b' });
|
|
249
259
|
});
|
|
250
260
|
});
|
|
251
261
|
|
|
@@ -282,7 +292,7 @@ describe('State Management', () => {
|
|
|
282
292
|
clearActiveRequest(paths, 'claude', 'wrong-id');
|
|
283
293
|
|
|
284
294
|
const state = loadState(paths);
|
|
285
|
-
expect(state.requests.claude).
|
|
295
|
+
expect(state.requests.claude).toMatchObject({ id: 'req-1', nonce: 'a' }); // Should still exist
|
|
286
296
|
});
|
|
287
297
|
|
|
288
298
|
it('clears when requestId matches', () => {
|
|
@@ -305,7 +315,7 @@ describe('State Management', () => {
|
|
|
305
315
|
|
|
306
316
|
const state = loadState(paths);
|
|
307
317
|
expect(state.requests.claude).toBeUndefined();
|
|
308
|
-
expect(state.requests.codex).
|
|
318
|
+
expect(state.requests.codex).toMatchObject({ id: '2', nonce: 'b' });
|
|
309
319
|
});
|
|
310
320
|
});
|
|
311
321
|
});
|
package/src/state.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface AgentRequestState {
|
|
|
15
15
|
|
|
16
16
|
export interface StateFile {
|
|
17
17
|
requests: Record<string, AgentRequestState>;
|
|
18
|
+
preambleCounters?: Record<string, number>; // agentName -> message count
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const DEFAULT_STATE: StateFile = { requests: {} };
|
|
@@ -52,7 +53,10 @@ export function cleanupState(paths: Paths, ttlSeconds: number): StateFile {
|
|
|
52
53
|
const now = Date.now();
|
|
53
54
|
|
|
54
55
|
const ttlMs = Math.max(1, ttlSeconds) * 1000;
|
|
55
|
-
const next: StateFile = {
|
|
56
|
+
const next: StateFile = {
|
|
57
|
+
requests: {},
|
|
58
|
+
preambleCounters: state.preambleCounters, // Preserve preamble counters
|
|
59
|
+
};
|
|
56
60
|
|
|
57
61
|
for (const [agent, req] of Object.entries(state.requests)) {
|
|
58
62
|
if (!req || typeof req.startedAtMs !== 'number') continue;
|
|
@@ -81,3 +85,26 @@ export function clearActiveRequest(paths: Paths, agent: string, requestId?: stri
|
|
|
81
85
|
delete state.requests[agent];
|
|
82
86
|
saveState(paths, state);
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the current preamble counter for an agent.
|
|
91
|
+
* Returns 0 if not set.
|
|
92
|
+
*/
|
|
93
|
+
export function getPreambleCounter(paths: Paths, agent: string): number {
|
|
94
|
+
const state = loadState(paths);
|
|
95
|
+
return state.preambleCounters?.[agent] ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Increment the preamble counter for an agent and return the new value.
|
|
100
|
+
*/
|
|
101
|
+
export function incrementPreambleCounter(paths: Paths, agent: string): number {
|
|
102
|
+
const state = loadState(paths);
|
|
103
|
+
if (!state.preambleCounters) {
|
|
104
|
+
state.preambleCounters = {};
|
|
105
|
+
}
|
|
106
|
+
const newCount = (state.preambleCounters[agent] ?? 0) + 1;
|
|
107
|
+
state.preambleCounters[agent] = newCount;
|
|
108
|
+
saveState(paths, state);
|
|
109
|
+
return newCount;
|
|
110
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -10,24 +10,27 @@ export interface AgentConfig {
|
|
|
10
10
|
export interface PaneEntry {
|
|
11
11
|
pane: string;
|
|
12
12
|
remark?: string;
|
|
13
|
+
preamble?: string; // Agent preamble (prepended to messages)
|
|
14
|
+
deny?: string[]; // Permission deny patterns
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface ConfigDefaults {
|
|
16
18
|
timeout: number; // seconds
|
|
17
19
|
pollInterval: number; // seconds
|
|
18
20
|
captureLines: number;
|
|
21
|
+
preambleEvery: number; // inject preamble every N messages (default: 3)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface GlobalConfig {
|
|
22
25
|
mode: 'polling' | 'wait';
|
|
23
26
|
preambleMode: 'always' | 'disabled';
|
|
24
27
|
defaults: ConfigDefaults;
|
|
25
|
-
agents: Record<string, AgentConfig>;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface LocalSettings {
|
|
29
31
|
mode?: 'polling' | 'wait';
|
|
30
32
|
preambleMode?: 'always' | 'disabled';
|
|
33
|
+
preambleEvery?: number; // local override for preamble frequency
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface LocalConfigFile {
|
|
@@ -82,7 +85,8 @@ export interface Tmux {
|
|
|
82
85
|
export interface WaitResult {
|
|
83
86
|
requestId: string;
|
|
84
87
|
nonce: string;
|
|
85
|
-
|
|
88
|
+
startMarker: string;
|
|
89
|
+
endMarker: string;
|
|
86
90
|
response: string;
|
|
87
91
|
}
|
|
88
92
|
|
package/src/ui.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { UI } from './types.js';
|
|
|
7
7
|
const isTTY = process.stdout.isTTY;
|
|
8
8
|
|
|
9
9
|
// Strip ANSI escape codes for accurate length calculation
|
|
10
|
-
const
|
|
10
|
+
const ansiEscape = String.fromCharCode(27);
|
|
11
|
+
const stripAnsi = (s: string) => s.replace(new RegExp(`${ansiEscape}\\[[0-9;]*m`, 'g'), '');
|
|
11
12
|
|
|
12
13
|
export const colors = {
|
|
13
14
|
red: (s: string) => (isTTY ? `\x1b[31m${s}\x1b[0m` : s),
|