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.
@@ -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).toBeDefined();
55
- expect(state.requests.claude?.nonce).toBe('abc123');
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).toBeDefined();
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).toBeDefined();
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).toBeDefined();
197
- expect(state.requests.claude?.id).toBe('req-1');
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).toBeDefined();
248
- expect(state.requests.codex).toBeDefined();
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).toBeDefined(); // Should still exist
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).toBeDefined();
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 = { requests: {} };
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
- marker: string;
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 stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
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),
package/src/version.ts CHANGED
@@ -14,7 +14,7 @@ function getVersion(): string {
14
14
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
15
15
  return pkg.version;
16
16
  } catch {
17
- return '2.0.0';
17
+ return '3.0.0';
18
18
  }
19
19
  }
20
20