@atercates/claude-deck 0.2.3 → 0.2.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.
@@ -1,375 +0,0 @@
1
- /**
2
- * Session Status Detection System
3
- *
4
- * States:
5
- * - "running" (GREEN): Sustained activity within cooldown period
6
- * - "waiting" (YELLOW): Cooldown expired, NOT acknowledged (needs attention)
7
- * - "idle" (GRAY): Cooldown expired, acknowledged (user saw it)
8
- * - "dead": Session doesn't exist
9
- *
10
- * Detection Strategy:
11
- * 1. Busy indicators + recent activity (highest priority - actively working)
12
- * 2. Waiting patterns - user input needed
13
- * 3. Spike detection - activity timestamp changes (2+ in 1s = sustained)
14
- * 4. Cooldown - 2s grace period after activity stops
15
- */
16
-
17
- import { exec } from "child_process";
18
- import { promisify } from "util";
19
-
20
- const execAsync = promisify(exec);
21
-
22
- // Configuration constants
23
- const CONFIG = {
24
- ACTIVITY_COOLDOWN_MS: 2000, // Grace period after activity
25
- SPIKE_WINDOW_MS: 1000, // Window to detect sustained activity
26
- SUSTAINED_THRESHOLD: 2, // Changes needed to confirm activity
27
- CACHE_VALIDITY_MS: 2000, // How long tmux cache is valid
28
- RECENT_ACTIVITY_MS: 120000, // Window for "recent" activity (2 min, tmux updates slowly)
29
- } as const;
30
-
31
- // Detection patterns
32
- const BUSY_INDICATORS = [
33
- "esc to interrupt",
34
- "(esc to interrupt)",
35
- "· esc to interrupt",
36
- ];
37
-
38
- const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
39
-
40
- const WHIMSICAL_WORDS = [
41
- "accomplishing",
42
- "actioning",
43
- "actualizing",
44
- "baking",
45
- "booping",
46
- "brewing",
47
- "calculating",
48
- "cerebrating",
49
- "channelling",
50
- "churning",
51
- "clauding",
52
- "coalescing",
53
- "cogitating",
54
- "combobulating",
55
- "computing",
56
- "concocting",
57
- "conjuring",
58
- "considering",
59
- "contemplating",
60
- "cooking",
61
- "crafting",
62
- "creating",
63
- "crunching",
64
- "deciphering",
65
- "deliberating",
66
- "determining",
67
- "discombobulating",
68
- "divining",
69
- "doing",
70
- "effecting",
71
- "elucidating",
72
- "enchanting",
73
- "envisioning",
74
- "finagling",
75
- "flibbertigibbeting",
76
- "forging",
77
- "forming",
78
- "frolicking",
79
- "generating",
80
- "germinating",
81
- "hatching",
82
- "herding",
83
- "honking",
84
- "hustling",
85
- "ideating",
86
- "imagining",
87
- "incubating",
88
- "inferring",
89
- "jiving",
90
- "manifesting",
91
- "marinating",
92
- "meandering",
93
- "moseying",
94
- "mulling",
95
- "mustering",
96
- "musing",
97
- "noodling",
98
- "percolating",
99
- "perusing",
100
- "philosophising",
101
- "pondering",
102
- "pontificating",
103
- "processing",
104
- "puttering",
105
- "puzzling",
106
- "reticulating",
107
- "ruminating",
108
- "scheming",
109
- "schlepping",
110
- "shimmying",
111
- "shucking",
112
- "simmering",
113
- "smooshing",
114
- "spelunking",
115
- "spinning",
116
- "stewing",
117
- "sussing",
118
- "synthesizing",
119
- "thinking",
120
- "tinkering",
121
- "transmuting",
122
- "unfurling",
123
- "unravelling",
124
- "vibing",
125
- "wandering",
126
- "whirring",
127
- "wibbling",
128
- "wizarding",
129
- "working",
130
- "wrangling",
131
- ];
132
-
133
- const WAITING_PATTERNS = [
134
- /\[Y\/n\]/i,
135
- /\[y\/N\]/i,
136
- /Allow\?/i,
137
- /Approve\?/i,
138
- /Continue\?/i,
139
- /Press Enter to/i,
140
- /waiting for input/i,
141
- /\(yes\/no\)/i,
142
- /Do you want to/i,
143
- /Enter to confirm.*Esc to cancel/i,
144
- />\s*1\.\s*Yes/,
145
- /Yes, allow all/i,
146
- /allow all edits/i,
147
- /allow all commands/i,
148
- ];
149
-
150
- export type SessionStatus = "running" | "waiting" | "idle" | "dead";
151
-
152
- interface StateTracker {
153
- lastChangeTime: number;
154
- acknowledged: boolean;
155
- lastActivityTimestamp: number;
156
- spikeWindowStart: number | null;
157
- spikeChangeCount: number;
158
- }
159
-
160
- interface SessionCache {
161
- data: Map<string, number>;
162
- updatedAt: number;
163
- }
164
-
165
- // Content analysis helpers
166
- function checkBusyIndicators(content: string): boolean {
167
- const lines = content.split("\n");
168
- // Focus on last 10 lines to avoid old scrollback false positives
169
- const recentContent = lines.slice(-10).join("\n").toLowerCase();
170
-
171
- // Check text indicators in recent lines
172
- if (BUSY_INDICATORS.some((ind) => recentContent.includes(ind))) return true;
173
-
174
- // Check whimsical words + "tokens" pattern in recent lines
175
- if (
176
- recentContent.includes("tokens") &&
177
- WHIMSICAL_WORDS.some((w) => recentContent.includes(w))
178
- )
179
- return true;
180
-
181
- // Check spinners in last 5 lines
182
- const last5 = lines.slice(-5).join("");
183
- if (SPINNER_CHARS.some((s) => last5.includes(s))) return true;
184
-
185
- return false;
186
- }
187
-
188
- function checkWaitingPatterns(content: string): boolean {
189
- const recentLines = content.split("\n").slice(-5).join("\n");
190
- return WAITING_PATTERNS.some((p) => p.test(recentLines));
191
- }
192
-
193
- class SessionStatusDetector {
194
- private trackers = new Map<string, StateTracker>();
195
- private cache: SessionCache = { data: new Map(), updatedAt: 0 };
196
-
197
- // Cache management
198
- async refreshCache(): Promise<void> {
199
- if (Date.now() - this.cache.updatedAt < CONFIG.CACHE_VALIDITY_MS) return;
200
-
201
- try {
202
- const { stdout } = await execAsync(
203
- `tmux list-sessions -F '#{session_name}\t#{session_activity}' 2>/dev/null || echo ""`
204
- );
205
-
206
- const newData = new Map<string, number>();
207
- for (const line of stdout.trim().split("\n")) {
208
- if (!line) continue;
209
- const [name, activity] = line.split("\t");
210
- if (name && activity) newData.set(name, parseInt(activity, 10) || 0);
211
- }
212
-
213
- this.cache = { data: newData, updatedAt: Date.now() };
214
- } catch {
215
- // Keep existing cache on error
216
- }
217
- }
218
-
219
- sessionExists(name: string): boolean {
220
- return this.cache.data.has(name);
221
- }
222
-
223
- getTimestamp(name: string): number {
224
- return this.cache.data.get(name) || 0;
225
- }
226
-
227
- async capturePane(name: string): Promise<string> {
228
- try {
229
- const { stdout } = await execAsync(
230
- `tmux capture-pane -t "${name}" -p 2>/dev/null || echo ""`
231
- );
232
- return stdout.trim();
233
- } catch {
234
- return "";
235
- }
236
- }
237
-
238
- private getTracker(name: string, timestamp: number): StateTracker {
239
- let tracker = this.trackers.get(name);
240
- if (!tracker) {
241
- tracker = {
242
- lastChangeTime: Date.now() - CONFIG.ACTIVITY_COOLDOWN_MS,
243
- acknowledged: true,
244
- lastActivityTimestamp: timestamp,
245
- spikeWindowStart: null,
246
- spikeChangeCount: 0,
247
- };
248
- this.trackers.set(name, tracker);
249
- }
250
- return tracker;
251
- }
252
-
253
- // Spike detection: filters single activity spikes from sustained activity
254
- private processSpikeDetection(
255
- tracker: StateTracker,
256
- currentTimestamp: number
257
- ): "running" | null {
258
- const now = Date.now();
259
- const timestampChanged = tracker.lastActivityTimestamp !== currentTimestamp;
260
-
261
- if (timestampChanged) {
262
- tracker.lastActivityTimestamp = currentTimestamp;
263
-
264
- const windowExpired =
265
- tracker.spikeWindowStart === null ||
266
- now - tracker.spikeWindowStart > CONFIG.SPIKE_WINDOW_MS;
267
-
268
- if (windowExpired) {
269
- // Start new detection window
270
- tracker.spikeWindowStart = now;
271
- tracker.spikeChangeCount = 1;
272
- } else {
273
- // Within window - count change
274
- tracker.spikeChangeCount++;
275
- if (tracker.spikeChangeCount >= CONFIG.SUSTAINED_THRESHOLD) {
276
- // Sustained activity confirmed
277
- tracker.lastChangeTime = now;
278
- tracker.acknowledged = false;
279
- tracker.spikeWindowStart = null;
280
- tracker.spikeChangeCount = 0;
281
- return "running";
282
- }
283
- }
284
- } else if (
285
- tracker.spikeChangeCount === 1 &&
286
- tracker.spikeWindowStart !== null
287
- ) {
288
- // Check if single spike should be filtered
289
- if (now - tracker.spikeWindowStart > CONFIG.SPIKE_WINDOW_MS) {
290
- tracker.spikeWindowStart = null;
291
- tracker.spikeChangeCount = 0;
292
- }
293
- }
294
-
295
- return null;
296
- }
297
-
298
- private isInSpikeWindow(tracker: StateTracker): boolean {
299
- return (
300
- tracker.spikeWindowStart !== null &&
301
- Date.now() - tracker.spikeWindowStart < CONFIG.SPIKE_WINDOW_MS
302
- );
303
- }
304
-
305
- private isInCooldown(tracker: StateTracker): boolean {
306
- return Date.now() - tracker.lastChangeTime < CONFIG.ACTIVITY_COOLDOWN_MS;
307
- }
308
-
309
- private getIdleOrWaiting(tracker: StateTracker): SessionStatus {
310
- return tracker.acknowledged ? "idle" : "waiting";
311
- }
312
-
313
- async getStatus(sessionName: string): Promise<SessionStatus> {
314
- await this.refreshCache();
315
-
316
- // Dead check
317
- if (!this.sessionExists(sessionName)) {
318
- this.trackers.delete(sessionName);
319
- return "dead";
320
- }
321
-
322
- const timestamp = this.getTimestamp(sessionName);
323
- const tracker = this.getTracker(sessionName, timestamp);
324
- const content = await this.capturePane(sessionName);
325
-
326
- // 1. Busy indicators in last 10 lines (highest priority - Claude is actively working)
327
- // No activity timestamp check needed since we only look at recent terminal lines
328
- if (checkBusyIndicators(content)) {
329
- tracker.lastChangeTime = Date.now();
330
- tracker.acknowledged = false;
331
- return "running";
332
- }
333
-
334
- // 2. Waiting patterns (only if not actively running)
335
- if (checkWaitingPatterns(content)) return "waiting";
336
-
337
- // 3. Spike detection
338
- const spikeResult = this.processSpikeDetection(tracker, timestamp);
339
- if (spikeResult) return spikeResult;
340
-
341
- // 4. During spike window, maintain stable status
342
- if (this.isInSpikeWindow(tracker)) {
343
- return this.isInCooldown(tracker)
344
- ? "running"
345
- : this.getIdleOrWaiting(tracker);
346
- }
347
-
348
- // 5. Cooldown check
349
- if (this.isInCooldown(tracker)) return "running";
350
-
351
- // 6. Cooldown expired
352
- return this.getIdleOrWaiting(tracker);
353
- }
354
-
355
- acknowledge(sessionName: string): void {
356
- const tracker = this.trackers.get(sessionName);
357
- if (tracker) tracker.acknowledged = true;
358
- }
359
-
360
- async getAllStatuses(names: string[]): Promise<Map<string, SessionStatus>> {
361
- await this.refreshCache();
362
- const results = await Promise.all(
363
- names.map(async (name) => ({ name, status: await this.getStatus(name) }))
364
- );
365
- return new Map(results.map((r) => [r.name, r.status]));
366
- }
367
-
368
- cleanup(): void {
369
- for (const [name] of this.trackers) {
370
- if (!this.sessionExists(name)) this.trackers.delete(name);
371
- }
372
- }
373
- }
374
-
375
- export const statusDetector = new SessionStatusDetector();