@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.
Files changed (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -1
  2. package/claude-plugin/hooks/hooks.json +41 -0
  3. package/claude-plugin/hooks/session-status.sh +13 -0
  4. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  5. package/dist/app.js +228 -28
  6. package/dist/components/CritterHeader.d.ts +7 -0
  7. package/dist/components/CritterHeader.js +81 -0
  8. package/dist/components/HelpOverlay.js +4 -2
  9. package/dist/components/List.d.ts +10 -1
  10. package/dist/components/List.js +14 -5
  11. package/dist/components/Panel.js +27 -1
  12. package/dist/components/SplashScreen.js +1 -1
  13. package/dist/hooks/useSessions.js +2 -2
  14. package/dist/index.js +41 -1
  15. package/dist/lib/auth/index.d.ts +2 -0
  16. package/dist/lib/auth/index.js +3 -0
  17. package/dist/lib/auth/linear.d.ts +20 -0
  18. package/dist/lib/auth/linear.js +79 -0
  19. package/dist/lib/auth/storage.d.ts +12 -0
  20. package/dist/lib/auth/storage.js +53 -0
  21. package/dist/lib/config.d.ts +13 -1
  22. package/dist/lib/config.js +51 -0
  23. package/dist/lib/context.d.ts +10 -0
  24. package/dist/lib/context.js +63 -0
  25. package/dist/lib/critters.d.ts +61 -0
  26. package/dist/lib/critters.js +365 -0
  27. package/dist/lib/hooks.d.ts +20 -0
  28. package/dist/lib/hooks.js +91 -0
  29. package/dist/lib/hotkeys.d.ts +1 -1
  30. package/dist/lib/hotkeys.js +28 -20
  31. package/dist/lib/issues/github.d.ts +11 -0
  32. package/dist/lib/issues/github.js +154 -0
  33. package/dist/lib/issues/index.d.ts +14 -0
  34. package/dist/lib/issues/index.js +27 -0
  35. package/dist/lib/issues/linear.d.ts +24 -0
  36. package/dist/lib/issues/linear.js +345 -0
  37. package/dist/lib/issues/types.d.ts +82 -0
  38. package/dist/lib/issues/types.js +2 -0
  39. package/dist/lib/paths.d.ts +3 -0
  40. package/dist/lib/paths.js +3 -0
  41. package/dist/lib/signals.d.ts +30 -0
  42. package/dist/lib/signals.js +104 -0
  43. package/dist/lib/tmux.d.ts +9 -2
  44. package/dist/lib/tmux.js +114 -18
  45. package/dist/mcp-server.js +161 -1
  46. package/dist/types.d.ts +23 -2
  47. package/dist/views/BarnContext.d.ts +5 -2
  48. package/dist/views/BarnContext.js +202 -21
  49. package/dist/views/CritterDetailView.d.ts +10 -0
  50. package/dist/views/CritterDetailView.js +117 -0
  51. package/dist/views/CritterLogsView.d.ts +8 -0
  52. package/dist/views/CritterLogsView.js +100 -0
  53. package/dist/views/GlobalDashboard.d.ts +2 -2
  54. package/dist/views/GlobalDashboard.js +20 -18
  55. package/dist/views/IssuesView.d.ts +2 -1
  56. package/dist/views/IssuesView.js +661 -98
  57. package/dist/views/LivestockDetailView.d.ts +2 -1
  58. package/dist/views/LivestockDetailView.js +19 -8
  59. package/dist/views/ProjectContext.d.ts +2 -2
  60. package/dist/views/ProjectContext.js +68 -25
  61. 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
+ }
@@ -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 text for a tmux window
37
+ * Get formatted status info for a tmux window
31
38
  */
32
- export declare function getWindowStatus(window: TmuxWindow): string;
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
- return parseInt(result.stdout.trim(), 10);
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
- return parseInt(result.stdout.trim(), 10);
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
- return parseInt(result.stdout.trim(), 10);
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 text for a tmux window
371
+ * Get formatted status info for a tmux window
300
372
  */
301
373
  export function getWindowStatus(window) {
302
- const isClaudeSession = window.name.includes('claude') && window.paneCurrentCommand === 'node';
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 window.paneTitle; // Shows spinner + task description
396
+ return {
397
+ text: window.paneTitle,
398
+ status: 'working',
399
+ icon: getStatusIcon('working'),
400
+ };
310
401
  }
311
- // Not working - show title with idle time if stale
312
- if (relativeTime !== 'now' && relativeTime !== '1m') {
313
- return `${window.paneTitle} (${relativeTime})`;
314
- }
315
- return window.paneTitle;
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
- return relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
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
- // Running a specific command
323
- return cmd;
418
+ return { text: cmd, status: 'working', icon: getStatusIcon('working') };
324
419
  }
325
420
  // At shell prompt - show idle time
326
- return relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
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
@@ -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
- type: string;
44
- status?: 'running' | 'stopped' | 'unknown';
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 {};