@colmbus72/yeehaw 0.4.2 → 0.6.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/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- package/package.json +5 -5
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type SessionStatus = 'working' | 'waiting' | 'idle' | 'error';
|
|
2
|
+
export interface SessionSignal {
|
|
3
|
+
status: SessionStatus;
|
|
4
|
+
updated: number;
|
|
5
|
+
}
|
|
6
|
+
export interface WindowStatusInfo {
|
|
7
|
+
text: string;
|
|
8
|
+
status: SessionStatus;
|
|
9
|
+
icon: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read signal file for a tmux pane
|
|
13
|
+
*/
|
|
14
|
+
export declare function readSignal(paneId: string): SessionSignal | null;
|
|
15
|
+
/**
|
|
16
|
+
* Get status icon for a session status
|
|
17
|
+
*/
|
|
18
|
+
export declare function getStatusIcon(status: SessionStatus): string;
|
|
19
|
+
/**
|
|
20
|
+
* Ensure the signals directory exists
|
|
21
|
+
*/
|
|
22
|
+
export declare function ensureSignalsDir(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Clean up signal file for a pane
|
|
25
|
+
*/
|
|
26
|
+
export declare function cleanupSignal(paneId: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Clean up all stale signal files (older than 1 hour)
|
|
29
|
+
*/
|
|
30
|
+
export declare function cleanupStaleSignals(): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { SIGNALS_DIR } from './paths.js';
|
|
4
|
+
const STATUS_ICONS = {
|
|
5
|
+
working: '⠿',
|
|
6
|
+
waiting: '◆',
|
|
7
|
+
idle: '○',
|
|
8
|
+
error: '✖',
|
|
9
|
+
};
|
|
10
|
+
const SIGNAL_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
/**
|
|
12
|
+
* Sanitize pane ID to create a safe filename
|
|
13
|
+
*/
|
|
14
|
+
function sanitizePaneId(paneId) {
|
|
15
|
+
return paneId.replace(/[^a-zA-Z0-9]/g, '_');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read signal file for a tmux pane
|
|
19
|
+
*/
|
|
20
|
+
export function readSignal(paneId) {
|
|
21
|
+
if (!existsSync(SIGNALS_DIR))
|
|
22
|
+
return null;
|
|
23
|
+
const filename = `${sanitizePaneId(paneId)}.json`;
|
|
24
|
+
const filepath = join(SIGNALS_DIR, filename);
|
|
25
|
+
if (!existsSync(filepath))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
29
|
+
const signal = JSON.parse(content);
|
|
30
|
+
// Check if signal is stale
|
|
31
|
+
const ageMs = Date.now() - signal.updated * 1000;
|
|
32
|
+
if (ageMs > SIGNAL_MAX_AGE_MS) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return signal;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get status icon for a session status
|
|
43
|
+
*/
|
|
44
|
+
export function getStatusIcon(status) {
|
|
45
|
+
return STATUS_ICONS[status];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Ensure the signals directory exists
|
|
49
|
+
*/
|
|
50
|
+
export function ensureSignalsDir() {
|
|
51
|
+
if (!existsSync(SIGNALS_DIR)) {
|
|
52
|
+
mkdirSync(SIGNALS_DIR, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Clean up signal file for a pane
|
|
57
|
+
*/
|
|
58
|
+
export function cleanupSignal(paneId) {
|
|
59
|
+
const filename = `${sanitizePaneId(paneId)}.json`;
|
|
60
|
+
const filepath = join(SIGNALS_DIR, filename);
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(filepath)) {
|
|
63
|
+
unlinkSync(filepath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore cleanup errors
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clean up all stale signal files (older than 1 hour)
|
|
72
|
+
*/
|
|
73
|
+
export function cleanupStaleSignals() {
|
|
74
|
+
if (!existsSync(SIGNALS_DIR))
|
|
75
|
+
return;
|
|
76
|
+
const STALE_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
try {
|
|
79
|
+
const files = readdirSync(SIGNALS_DIR).filter(f => f.endsWith('.json'));
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const filepath = join(SIGNALS_DIR, file);
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
84
|
+
const signal = JSON.parse(content);
|
|
85
|
+
const ageMs = now - signal.updated * 1000;
|
|
86
|
+
if (ageMs > STALE_AGE_MS) {
|
|
87
|
+
unlinkSync(filepath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Delete malformed files
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(filepath);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Ignore errors
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/lib/tmux.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { type WindowStatusInfo } from './signals.js';
|
|
2
|
+
export type { WindowStatusInfo, SessionStatus } from './signals.js';
|
|
1
3
|
export declare const YEEHAW_SESSION = "yeehaw";
|
|
4
|
+
export type WindowType = 'claude' | 'shell' | 'ssh' | '';
|
|
2
5
|
export interface TmuxWindow {
|
|
3
6
|
index: number;
|
|
4
7
|
name: string;
|
|
5
8
|
active: boolean;
|
|
9
|
+
paneId: string;
|
|
6
10
|
paneTitle: string;
|
|
7
11
|
paneCurrentCommand: string;
|
|
8
12
|
windowActivity: number;
|
|
13
|
+
type: WindowType;
|
|
9
14
|
}
|
|
10
15
|
export declare function hasTmux(): boolean;
|
|
11
16
|
export declare function isInsideYeehawSession(): boolean;
|
|
@@ -19,17 +24,19 @@ export declare function setupStatusBarHooks(): void;
|
|
|
19
24
|
export declare function ensureCorrectStatusBar(): void;
|
|
20
25
|
export declare function attachToYeehaw(): void;
|
|
21
26
|
export declare function createClaudeWindow(workingDir: string, windowName: string): number;
|
|
27
|
+
export declare function createClaudeWindowWithPrompt(workingDir: string, windowName: string, systemPrompt: string): number;
|
|
22
28
|
export declare function createShellWindow(workingDir: string, windowName: string, shell?: string): number;
|
|
23
29
|
export declare function createSshWindow(windowName: string, host: string, user: string, port: number, identityFile: string, remotePath: string): number;
|
|
24
30
|
export declare function detachFromSession(): void;
|
|
25
31
|
export declare function killYeehawSession(): void;
|
|
32
|
+
export declare function restartYeehaw(): void;
|
|
26
33
|
export declare function switchToWindow(windowIndex: number): void;
|
|
27
34
|
export declare function listYeehawWindows(): TmuxWindow[];
|
|
28
35
|
export declare function killWindow(windowIndex: number): void;
|
|
29
36
|
/**
|
|
30
|
-
* Get formatted status
|
|
37
|
+
* Get formatted status info for a tmux window
|
|
31
38
|
*/
|
|
32
|
-
export declare function getWindowStatus(window: TmuxWindow):
|
|
39
|
+
export declare function getWindowStatus(window: TmuxWindow): WindowStatusInfo;
|
|
33
40
|
export declare function updateStatusBar(projectName?: string): void;
|
|
34
41
|
export declare function enterRemoteMode(barnName: string, host: string, user: string, port: number, identityFile: string): number;
|
|
35
42
|
export declare function exitRemoteMode(): void;
|
package/dist/lib/tmux.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { writeTmuxConfig, TMUX_CONFIG_PATH } from './tmux-config.js';
|
|
6
6
|
import { shellEscape } from './shell.js';
|
|
7
|
+
import { readSignal, getStatusIcon } from './signals.js';
|
|
7
8
|
// Get the path to the MCP server (it's in dist/, not dist/lib/)
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
@@ -130,23 +131,44 @@ export function attachToYeehaw() {
|
|
|
130
131
|
}
|
|
131
132
|
// All yeehaw MCP tools that should be auto-approved
|
|
132
133
|
const YEEHAW_MCP_TOOLS = [
|
|
134
|
+
// Project management
|
|
133
135
|
'mcp__yeehaw__list_projects',
|
|
134
136
|
'mcp__yeehaw__get_project',
|
|
135
137
|
'mcp__yeehaw__create_project',
|
|
136
138
|
'mcp__yeehaw__update_project',
|
|
137
139
|
'mcp__yeehaw__delete_project',
|
|
140
|
+
// Livestock management
|
|
138
141
|
'mcp__yeehaw__add_livestock',
|
|
139
142
|
'mcp__yeehaw__remove_livestock',
|
|
143
|
+
'mcp__yeehaw__read_livestock_logs',
|
|
144
|
+
'mcp__yeehaw__read_livestock_env',
|
|
145
|
+
// Barn management
|
|
140
146
|
'mcp__yeehaw__list_barns',
|
|
141
147
|
'mcp__yeehaw__get_barn',
|
|
142
148
|
'mcp__yeehaw__create_barn',
|
|
143
149
|
'mcp__yeehaw__update_barn',
|
|
144
150
|
'mcp__yeehaw__delete_barn',
|
|
151
|
+
// Critter management
|
|
152
|
+
'mcp__yeehaw__add_critter',
|
|
153
|
+
'mcp__yeehaw__remove_critter',
|
|
154
|
+
'mcp__yeehaw__read_critter_logs',
|
|
155
|
+
'mcp__yeehaw__discover_critters',
|
|
156
|
+
// Wiki management
|
|
145
157
|
'mcp__yeehaw__get_wiki',
|
|
158
|
+
'mcp__yeehaw__get_wiki_section',
|
|
146
159
|
'mcp__yeehaw__add_wiki_section',
|
|
147
160
|
'mcp__yeehaw__update_wiki_section',
|
|
148
161
|
'mcp__yeehaw__delete_wiki_section',
|
|
149
162
|
];
|
|
163
|
+
/**
|
|
164
|
+
* Set the window type option for a window (used for reliable window type detection)
|
|
165
|
+
*/
|
|
166
|
+
function setWindowType(windowIndex, type) {
|
|
167
|
+
execaSync('tmux', [
|
|
168
|
+
'set-option', '-w', '-t', `${YEEHAW_SESSION}:${windowIndex}`,
|
|
169
|
+
'@yeehaw_type', type,
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
150
172
|
export function createClaudeWindow(workingDir, windowName) {
|
|
151
173
|
// Build MCP config for yeehaw server
|
|
152
174
|
const mcpConfig = JSON.stringify({
|
|
@@ -175,7 +197,43 @@ export function createClaudeWindow(workingDir, windowName) {
|
|
|
175
197
|
const result = execaSync('tmux', [
|
|
176
198
|
'display-message', '-p', '#{window_index}'
|
|
177
199
|
]);
|
|
178
|
-
|
|
200
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
201
|
+
// Mark this window as a Claude session
|
|
202
|
+
setWindowType(windowIndex, 'claude');
|
|
203
|
+
return windowIndex;
|
|
204
|
+
}
|
|
205
|
+
export function createClaudeWindowWithPrompt(workingDir, windowName, systemPrompt) {
|
|
206
|
+
// Build MCP config for yeehaw server
|
|
207
|
+
const mcpConfig = JSON.stringify({
|
|
208
|
+
mcpServers: {
|
|
209
|
+
yeehaw: {
|
|
210
|
+
command: 'node',
|
|
211
|
+
args: [MCP_SERVER_PATH],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
// Build allowed tools list for auto-approval
|
|
216
|
+
const allowedTools = YEEHAW_MCP_TOOLS.join(',');
|
|
217
|
+
// Escape the system prompt for shell - use single quotes and escape any single quotes in content
|
|
218
|
+
const escapedPrompt = systemPrompt.replace(/'/g, "'\\''");
|
|
219
|
+
// Create new window running claude with yeehaw MCP server and system prompt
|
|
220
|
+
const claudeCmd = `claude --mcp-config ${shellEscape(mcpConfig)} --allowedTools ${shellEscape(allowedTools)} --plugin-dir ${shellEscape(CLAUDE_PLUGIN_PATH)} --system-prompt '${escapedPrompt}'`;
|
|
221
|
+
execaSync('tmux', [
|
|
222
|
+
'new-window',
|
|
223
|
+
'-a',
|
|
224
|
+
'-t', YEEHAW_SESSION,
|
|
225
|
+
'-n', windowName,
|
|
226
|
+
'-c', workingDir,
|
|
227
|
+
claudeCmd,
|
|
228
|
+
]);
|
|
229
|
+
// Get the window index we just created
|
|
230
|
+
const result = execaSync('tmux', [
|
|
231
|
+
'display-message', '-p', '#{window_index}'
|
|
232
|
+
]);
|
|
233
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
234
|
+
// Mark this window as a Claude session
|
|
235
|
+
setWindowType(windowIndex, 'claude');
|
|
236
|
+
return windowIndex;
|
|
179
237
|
}
|
|
180
238
|
export function createShellWindow(workingDir, windowName, shell) {
|
|
181
239
|
// Use the user's configured shell from $SHELL, fallback to /bin/bash
|
|
@@ -194,7 +252,10 @@ export function createShellWindow(workingDir, windowName, shell) {
|
|
|
194
252
|
const result = execaSync('tmux', [
|
|
195
253
|
'display-message', '-p', '#{window_index}'
|
|
196
254
|
]);
|
|
197
|
-
|
|
255
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
256
|
+
// Mark this window as a shell session
|
|
257
|
+
setWindowType(windowIndex, 'shell');
|
|
258
|
+
return windowIndex;
|
|
198
259
|
}
|
|
199
260
|
export function createSshWindow(windowName, host, user, port, identityFile, remotePath) {
|
|
200
261
|
// Two levels of escaping needed:
|
|
@@ -222,7 +283,10 @@ export function createSshWindow(windowName, host, user, port, identityFile, remo
|
|
|
222
283
|
const result = execaSync('tmux', [
|
|
223
284
|
'display-message', '-p', '#{window_index}'
|
|
224
285
|
]);
|
|
225
|
-
|
|
286
|
+
const windowIndex = parseInt(result.stdout.trim(), 10);
|
|
287
|
+
// Mark this window as an SSH session
|
|
288
|
+
setWindowType(windowIndex, 'ssh');
|
|
289
|
+
return windowIndex;
|
|
226
290
|
}
|
|
227
291
|
export function detachFromSession() {
|
|
228
292
|
execaSync('tmux', ['detach-client']);
|
|
@@ -235,29 +299,37 @@ export function killYeehawSession() {
|
|
|
235
299
|
// Session might already be dead
|
|
236
300
|
}
|
|
237
301
|
}
|
|
302
|
+
export function restartYeehaw() {
|
|
303
|
+
// Respawn window 0 with a fresh yeehaw process
|
|
304
|
+
// This kills the current process but preserves all other windows
|
|
305
|
+
execaSync('tmux', ['respawn-window', '-k', '-t', `${YEEHAW_SESSION}:0`, 'yeehaw']);
|
|
306
|
+
}
|
|
238
307
|
export function switchToWindow(windowIndex) {
|
|
239
308
|
execaSync('tmux', ['select-window', '-t', `${YEEHAW_SESSION}:${windowIndex}`]);
|
|
240
309
|
}
|
|
241
310
|
export function listYeehawWindows() {
|
|
242
311
|
try {
|
|
243
312
|
// Use tab as delimiter since pane_title can contain colons
|
|
313
|
+
// Include @yeehaw_type window option for reliable window type detection
|
|
244
314
|
const result = execaSync('tmux', [
|
|
245
315
|
'list-windows',
|
|
246
316
|
'-t', YEEHAW_SESSION,
|
|
247
|
-
'-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}',
|
|
317
|
+
'-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_id}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}\t#{@yeehaw_type}',
|
|
248
318
|
]);
|
|
249
319
|
return result.stdout
|
|
250
320
|
.split('\n')
|
|
251
321
|
.filter(Boolean)
|
|
252
322
|
.map((line) => {
|
|
253
|
-
const [index, name, active, paneTitle, paneCurrentCommand, windowActivity] = line.split('\t');
|
|
323
|
+
const [index, name, active, paneId, paneTitle, paneCurrentCommand, windowActivity, type] = line.split('\t');
|
|
254
324
|
return {
|
|
255
325
|
index: parseInt(index, 10),
|
|
256
326
|
name,
|
|
257
327
|
active: active === '1',
|
|
328
|
+
paneId: paneId || '',
|
|
258
329
|
paneTitle: paneTitle || '',
|
|
259
330
|
paneCurrentCommand: paneCurrentCommand || '',
|
|
260
331
|
windowActivity: parseInt(windowActivity, 10) || 0,
|
|
332
|
+
type: (type || ''),
|
|
261
333
|
};
|
|
262
334
|
});
|
|
263
335
|
}
|
|
@@ -296,34 +368,58 @@ function isClaudeWorking(paneTitle) {
|
|
|
296
368
|
return spinnerChars.some(char => paneTitle.startsWith(char));
|
|
297
369
|
}
|
|
298
370
|
/**
|
|
299
|
-
* Get formatted status
|
|
371
|
+
* Get formatted status info for a tmux window
|
|
300
372
|
*/
|
|
301
373
|
export function getWindowStatus(window) {
|
|
302
|
-
const isClaudeSession = window.
|
|
374
|
+
const isClaudeSession = window.type === 'claude';
|
|
303
375
|
const relativeTime = formatRelativeTime(window.windowActivity);
|
|
376
|
+
// Check for signal file first (written by Claude hooks)
|
|
377
|
+
if (isClaudeSession && window.paneId) {
|
|
378
|
+
const signal = readSignal(window.paneId);
|
|
379
|
+
if (signal) {
|
|
380
|
+
const icon = getStatusIcon(signal.status);
|
|
381
|
+
const text = signal.status === 'waiting'
|
|
382
|
+
? 'Waiting for input'
|
|
383
|
+
: signal.status === 'working'
|
|
384
|
+
? window.paneTitle || 'Working...'
|
|
385
|
+
: signal.status === 'error'
|
|
386
|
+
? 'Error'
|
|
387
|
+
: `idle ${relativeTime}`;
|
|
388
|
+
return { text: `${icon} ${text}`, status: signal.status, icon };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Fallback to tmux-native detection for Claude sessions
|
|
304
392
|
if (isClaudeSession) {
|
|
305
|
-
// For Claude sessions, use pane title (contains task context)
|
|
306
393
|
if (window.paneTitle) {
|
|
307
394
|
const working = isClaudeWorking(window.paneTitle);
|
|
308
395
|
if (working) {
|
|
309
|
-
return
|
|
396
|
+
return {
|
|
397
|
+
text: window.paneTitle,
|
|
398
|
+
status: 'working',
|
|
399
|
+
icon: getStatusIcon('working'),
|
|
400
|
+
};
|
|
310
401
|
}
|
|
311
|
-
// Not working -
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
402
|
+
// Not working - likely idle
|
|
403
|
+
const text = relativeTime !== 'now' && relativeTime !== '1m'
|
|
404
|
+
? `${window.paneTitle} (${relativeTime})`
|
|
405
|
+
: window.paneTitle;
|
|
406
|
+
return { text, status: 'idle', icon: getStatusIcon('idle') };
|
|
316
407
|
}
|
|
317
|
-
|
|
408
|
+
const text = relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
|
|
409
|
+
return { text: `○ ${text}`, status: 'idle', icon: '○' };
|
|
410
|
+
}
|
|
411
|
+
// For shell sessions, check if pane is dead
|
|
412
|
+
if (window.paneCurrentCommand === '') {
|
|
413
|
+
return { text: '✖ disconnected', status: 'error', icon: '✖' };
|
|
318
414
|
}
|
|
319
415
|
// For shell sessions, show current command
|
|
320
416
|
const cmd = window.paneCurrentCommand;
|
|
321
417
|
if (cmd && cmd !== 'zsh' && cmd !== 'bash' && cmd !== 'sh' && cmd !== 'fish') {
|
|
322
|
-
|
|
323
|
-
return cmd;
|
|
418
|
+
return { text: cmd, status: 'working', icon: getStatusIcon('working') };
|
|
324
419
|
}
|
|
325
420
|
// At shell prompt - show idle time
|
|
326
|
-
|
|
421
|
+
const text = relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
|
|
422
|
+
return { text: `○ ${text}`, status: 'idle', icon: '○' };
|
|
327
423
|
}
|
|
328
424
|
export function updateStatusBar(projectName) {
|
|
329
425
|
const left = projectName
|
package/dist/mcp-server.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
24
24
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
25
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
26
|
-
import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, } from './lib/config.js';
|
|
26
|
+
import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, addCritterToBarn, removeCritterFromBarn, getCritter, } from './lib/config.js';
|
|
27
27
|
import { readLivestockLogs, readLivestockEnv } from './lib/livestock.js';
|
|
28
|
+
import { readCritterLogs, discoverCritters } from './lib/critters.js';
|
|
28
29
|
import { requireString, optionalString, optionalNumber, optionalBoolean, } from './lib/mcp-validation.js';
|
|
29
30
|
const server = new Server({
|
|
30
31
|
name: 'yeehaw',
|
|
@@ -381,6 +382,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
381
382
|
required: ['project', 'title'],
|
|
382
383
|
},
|
|
383
384
|
},
|
|
385
|
+
// Critter tools
|
|
386
|
+
{
|
|
387
|
+
name: 'add_critter',
|
|
388
|
+
description: 'Add a critter (system service like mysql, redis, nginx) to a barn',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
393
|
+
name: { type: 'string', description: 'Critter name (user-friendly, e.g., "mysql", "redis-cache")' },
|
|
394
|
+
service: { type: 'string', description: 'systemd service name (e.g., "mysql.service")' },
|
|
395
|
+
config_path: { type: 'string', description: 'Path to config file (optional)' },
|
|
396
|
+
log_path: { type: 'string', description: 'Custom log path if not using journald (optional)' },
|
|
397
|
+
use_journald: { type: 'boolean', description: 'Use journalctl for logs (default: true)' },
|
|
398
|
+
},
|
|
399
|
+
required: ['barn', 'name', 'service'],
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'remove_critter',
|
|
404
|
+
description: 'Remove a critter from a barn',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
409
|
+
name: { type: 'string', description: 'Critter name to remove' },
|
|
410
|
+
},
|
|
411
|
+
required: ['barn', 'name'],
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'read_critter_logs',
|
|
416
|
+
description: 'Read logs from a critter (via journald or custom path)',
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
barn: { type: 'string', description: 'Barn name' },
|
|
421
|
+
critter: { type: 'string', description: 'Critter name' },
|
|
422
|
+
lines: { type: 'number', description: 'Last N lines (default: 100)' },
|
|
423
|
+
pattern: { type: 'string', description: 'Grep pattern to filter logs (case-insensitive)' },
|
|
424
|
+
},
|
|
425
|
+
required: ['barn', 'critter'],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'discover_critters',
|
|
430
|
+
description: 'Scan a barn for running services and return suggestions for critters to add',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
barn: { type: 'string', description: 'Barn name to scan' },
|
|
435
|
+
},
|
|
436
|
+
required: ['barn'],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
384
439
|
],
|
|
385
440
|
};
|
|
386
441
|
});
|
|
@@ -851,6 +906,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
851
906
|
content: [{ type: 'text', text: `Deleted wiki section '${title}' from project '${project.name}'` }],
|
|
852
907
|
};
|
|
853
908
|
}
|
|
909
|
+
// Critter operations
|
|
910
|
+
case 'add_critter': {
|
|
911
|
+
const barnName = requireString(args, 'barn');
|
|
912
|
+
const critterName = requireString(args, 'name');
|
|
913
|
+
const service = requireString(args, 'service');
|
|
914
|
+
const config_path = optionalString(args, 'config_path');
|
|
915
|
+
const log_path = optionalString(args, 'log_path');
|
|
916
|
+
const use_journald = optionalBoolean(args, 'use_journald', true);
|
|
917
|
+
const critter = {
|
|
918
|
+
name: critterName,
|
|
919
|
+
service,
|
|
920
|
+
config_path,
|
|
921
|
+
log_path,
|
|
922
|
+
use_journald,
|
|
923
|
+
};
|
|
924
|
+
try {
|
|
925
|
+
addCritterToBarn(barnName, critter);
|
|
926
|
+
return {
|
|
927
|
+
content: [{ type: 'text', text: `Added critter '${critterName}' (${service}) to barn '${barnName}'` }],
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
return {
|
|
932
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
933
|
+
isError: true,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
case 'remove_critter': {
|
|
938
|
+
const barnName = requireString(args, 'barn');
|
|
939
|
+
const critterName = requireString(args, 'name');
|
|
940
|
+
try {
|
|
941
|
+
const removed = removeCritterFromBarn(barnName, critterName);
|
|
942
|
+
if (!removed) {
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
|
|
945
|
+
isError: true,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
content: [{ type: 'text', text: `Removed critter '${critterName}' from barn '${barnName}'` }],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
return {
|
|
954
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
955
|
+
isError: true,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
case 'read_critter_logs': {
|
|
960
|
+
const barnName = requireString(args, 'barn');
|
|
961
|
+
const critterName = requireString(args, 'critter');
|
|
962
|
+
const lines = optionalNumber(args, 'lines');
|
|
963
|
+
const pattern = optionalString(args, 'pattern');
|
|
964
|
+
const barn = loadBarn(barnName);
|
|
965
|
+
if (!barn) {
|
|
966
|
+
return {
|
|
967
|
+
content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
|
|
968
|
+
isError: true,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const critter = getCritter(barnName, critterName);
|
|
972
|
+
if (!critter) {
|
|
973
|
+
return {
|
|
974
|
+
content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
|
|
975
|
+
isError: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const result = await readCritterLogs(critter, barn, { lines, pattern });
|
|
979
|
+
if (result.error) {
|
|
980
|
+
return {
|
|
981
|
+
content: [{ type: 'text', text: result.error }],
|
|
982
|
+
isError: true,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
content: [{ type: 'text', text: result.content }],
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
case 'discover_critters': {
|
|
990
|
+
const barnName = requireString(args, 'barn');
|
|
991
|
+
const barn = loadBarn(barnName);
|
|
992
|
+
if (!barn) {
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
|
|
995
|
+
isError: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const result = await discoverCritters(barn);
|
|
999
|
+
if (result.error) {
|
|
1000
|
+
return {
|
|
1001
|
+
content: [{ type: 'text', text: result.error }],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (result.critters.length === 0) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{ type: 'text', text: 'No interesting services discovered on this barn' }],
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
content: [{ type: 'text', text: JSON.stringify(result.critters, null, 2) }],
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
854
1014
|
default:
|
|
855
1015
|
return {
|
|
856
1016
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/dist/types.d.ts
CHANGED
|
@@ -24,11 +24,21 @@ export interface Project {
|
|
|
24
24
|
gradientInverted?: boolean;
|
|
25
25
|
livestock?: Livestock[];
|
|
26
26
|
wiki?: WikiSection[];
|
|
27
|
+
issueProvider?: IssueProviderConfig;
|
|
27
28
|
}
|
|
28
29
|
export interface WikiSection {
|
|
29
30
|
title: string;
|
|
30
31
|
content: string;
|
|
31
32
|
}
|
|
33
|
+
export type IssueProviderConfig = {
|
|
34
|
+
type: 'github';
|
|
35
|
+
} | {
|
|
36
|
+
type: 'linear';
|
|
37
|
+
teamId?: string;
|
|
38
|
+
teamName?: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'none';
|
|
41
|
+
};
|
|
32
42
|
export interface Livestock {
|
|
33
43
|
name: string;
|
|
34
44
|
path: string;
|
|
@@ -40,8 +50,11 @@ export interface Livestock {
|
|
|
40
50
|
}
|
|
41
51
|
export interface Critter {
|
|
42
52
|
name: string;
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
service: string;
|
|
54
|
+
service_path?: string;
|
|
55
|
+
config_path?: string;
|
|
56
|
+
log_path?: string;
|
|
57
|
+
use_journald?: boolean;
|
|
45
58
|
}
|
|
46
59
|
export interface Barn {
|
|
47
60
|
name: string;
|
|
@@ -90,6 +103,14 @@ export type AppView = {
|
|
|
90
103
|
livestock: Livestock;
|
|
91
104
|
source: 'project' | 'barn';
|
|
92
105
|
sourceBarn?: Barn;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'critter';
|
|
108
|
+
barn: Barn;
|
|
109
|
+
critter: Critter;
|
|
110
|
+
} | {
|
|
111
|
+
type: 'critter-logs';
|
|
112
|
+
barn: Barn;
|
|
113
|
+
critter: Critter;
|
|
93
114
|
} | {
|
|
94
115
|
type: 'night-sky';
|
|
95
116
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Barn, Project, Livestock } from '../types.js';
|
|
1
|
+
import type { Barn, Project, Livestock, Critter } from '../types.js';
|
|
2
2
|
import type { TmuxWindow } from '../lib/tmux.js';
|
|
3
3
|
interface LivestockWithProject {
|
|
4
4
|
project: Project;
|
|
@@ -17,6 +17,9 @@ interface BarnContextProps {
|
|
|
17
17
|
onDeleteBarn: (barnName: string) => void;
|
|
18
18
|
onAddLivestock: (project: Project, livestock: Livestock) => void;
|
|
19
19
|
onRemoveLivestock: (project: Project, livestockName: string) => void;
|
|
20
|
+
onAddCritter: (critter: Critter) => void;
|
|
21
|
+
onRemoveCritter: (critterName: string) => void;
|
|
22
|
+
onSelectCritter: (critter: Critter) => void;
|
|
20
23
|
}
|
|
21
|
-
export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, onSelectCritter, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
|
|
22
25
|
export {};
|