@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,365 @@
1
+ import { execa } from 'execa';
2
+ import { buildSshCommand } from './livestock.js';
3
+ import { shellEscape } from './shell.js';
4
+ import { getErrorMessage } from './errors.js';
5
+ /**
6
+ * Services that are interesting for developers (databases, web servers, etc.)
7
+ */
8
+ const INTERESTING_SERVICE_PATTERNS = [
9
+ // Databases
10
+ 'mysql', 'mariadb', 'postgres', 'postgresql', 'mongodb', 'mongo',
11
+ // Caches
12
+ 'redis', 'memcached',
13
+ // Web servers
14
+ 'nginx', 'apache', 'httpd', 'caddy',
15
+ // PHP
16
+ 'php-fpm', 'php7', 'php8',
17
+ // Python
18
+ 'gunicorn', 'uvicorn', 'celery',
19
+ // Node
20
+ 'node', 'pm2',
21
+ // Mail
22
+ 'postfix', 'dovecot',
23
+ // Queue
24
+ 'rabbitmq', 'kafka',
25
+ // Search
26
+ 'elasticsearch', 'opensearch', 'meilisearch',
27
+ // Other
28
+ 'supervisor', 'docker',
29
+ ];
30
+ /**
31
+ * Check if a service name matches any interesting pattern
32
+ */
33
+ function isInterestingService(serviceName) {
34
+ const lower = serviceName.toLowerCase();
35
+ return INTERESTING_SERVICE_PATTERNS.some(pattern => lower.includes(pattern));
36
+ }
37
+ /**
38
+ * Extract a friendly name from a service name
39
+ * e.g., "mysql.service" -> "mysql", "php8.1-fpm.service" -> "php8.1-fpm"
40
+ */
41
+ function extractServiceName(service) {
42
+ return service.replace(/\.service$/, '');
43
+ }
44
+ /**
45
+ * Read logs from a critter (via journald or custom path)
46
+ */
47
+ export async function readCritterLogs(critter, barn, options = {}) {
48
+ const { lines = 100, pattern } = options;
49
+ const escapedLines = String(lines);
50
+ let cmd;
51
+ if (critter.use_journald !== false) {
52
+ // Use journalctl
53
+ const escapedService = shellEscape(critter.service);
54
+ cmd = `journalctl -u ${escapedService} -n ${escapedLines} --no-pager`;
55
+ if (pattern) {
56
+ const escapedPattern = shellEscape(pattern);
57
+ cmd += ` | grep -i ${escapedPattern} || true`;
58
+ }
59
+ }
60
+ else if (critter.log_path) {
61
+ // Use custom log path
62
+ const escapedLogPath = shellEscape(critter.log_path);
63
+ cmd = `tail -n ${escapedLines} ${escapedLogPath}`;
64
+ if (pattern) {
65
+ const escapedPattern = shellEscape(pattern);
66
+ cmd += ` | grep -i ${escapedPattern} || true`;
67
+ }
68
+ }
69
+ else {
70
+ return { content: '', error: 'Critter has no log_path and use_journald is disabled' };
71
+ }
72
+ // Local barn
73
+ if (barn.name === 'local') {
74
+ try {
75
+ const result = await execa('sh', ['-c', cmd]);
76
+ if (!result.stdout.trim()) {
77
+ return { content: '', error: `No logs found for ${critter.name}` };
78
+ }
79
+ return { content: result.stdout };
80
+ }
81
+ catch (err) {
82
+ return { content: '', error: `Failed to read logs: ${getErrorMessage(err)}` };
83
+ }
84
+ }
85
+ // Remote barn - SSH
86
+ if (!barn.host || !barn.user) {
87
+ return { content: '', error: `Barn '${barn.name}' is not configured for SSH` };
88
+ }
89
+ try {
90
+ const sshArgs = buildSshCommand(barn);
91
+ const result = await execa(sshArgs[0], [...sshArgs.slice(1), cmd]);
92
+ if (!result.stdout.trim()) {
93
+ return { content: '', error: `No logs found for ${critter.name}` };
94
+ }
95
+ return { content: result.stdout };
96
+ }
97
+ catch (err) {
98
+ return { content: '', error: `SSH error: ${getErrorMessage(err)}` };
99
+ }
100
+ }
101
+ /**
102
+ * Parse systemctl show output into key-value pairs
103
+ */
104
+ function parseSystemctlShow(output) {
105
+ const result = {};
106
+ for (const line of output.split('\n')) {
107
+ const eqIndex = line.indexOf('=');
108
+ if (eqIndex !== -1) {
109
+ const key = line.slice(0, eqIndex);
110
+ const value = line.slice(eqIndex + 1);
111
+ result[key] = value;
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ /**
117
+ * Discover critters (running services) on a barn
118
+ */
119
+ export async function discoverCritters(barn) {
120
+ // Command to list running services
121
+ const listCmd = 'systemctl list-units --type=service --state=running --no-pager --plain';
122
+ let output;
123
+ // Local barn
124
+ if (barn.name === 'local') {
125
+ try {
126
+ const result = await execa('sh', ['-c', listCmd]);
127
+ output = result.stdout;
128
+ }
129
+ catch (err) {
130
+ return { critters: [], error: `Failed to list services: ${getErrorMessage(err)}` };
131
+ }
132
+ }
133
+ else {
134
+ // Remote barn - SSH
135
+ if (!barn.host || !barn.user) {
136
+ return { critters: [], error: `Barn '${barn.name}' is not configured for SSH` };
137
+ }
138
+ try {
139
+ const sshArgs = buildSshCommand(barn);
140
+ const result = await execa(sshArgs[0], [...sshArgs.slice(1), listCmd]);
141
+ output = result.stdout;
142
+ }
143
+ catch (err) {
144
+ return { critters: [], error: `SSH error: ${getErrorMessage(err)}` };
145
+ }
146
+ }
147
+ // Parse the output - each line: "unit.service loaded active running description"
148
+ const lines = output.split('\n').filter(line => line.trim());
149
+ const discovered = [];
150
+ for (const line of lines) {
151
+ const parts = line.trim().split(/\s+/);
152
+ if (parts.length < 1)
153
+ continue;
154
+ const service = parts[0];
155
+ if (!service.endsWith('.service'))
156
+ continue;
157
+ if (!isInterestingService(service))
158
+ continue;
159
+ const suggested_name = extractServiceName(service);
160
+ // Try to get more details about this service
161
+ let binary;
162
+ let config_path;
163
+ const showCmd = `systemctl show ${shellEscape(service)} --property=ExecStart`;
164
+ try {
165
+ let showOutput;
166
+ if (barn.name === 'local') {
167
+ const showResult = await execa('sh', ['-c', showCmd]);
168
+ showOutput = showResult.stdout;
169
+ }
170
+ else {
171
+ const sshArgs = buildSshCommand(barn);
172
+ const showResult = await execa(sshArgs[0], [...sshArgs.slice(1), showCmd]);
173
+ showOutput = showResult.stdout;
174
+ }
175
+ const props = parseSystemctlShow(showOutput);
176
+ // Extract binary from ExecStart (format: { path=/usr/bin/mysqld ; argv[]=... })
177
+ if (props.ExecStart) {
178
+ const pathMatch = props.ExecStart.match(/path=([^\s;]+)/);
179
+ if (pathMatch) {
180
+ binary = pathMatch[1];
181
+ }
182
+ // Try to find config flags like --config= or -c
183
+ const configMatch = props.ExecStart.match(/--config[=\s]([^\s;]+)/);
184
+ if (configMatch) {
185
+ config_path = configMatch[1];
186
+ }
187
+ }
188
+ }
189
+ catch {
190
+ // Ignore errors getting details - just use basic info
191
+ }
192
+ discovered.push({
193
+ service,
194
+ suggested_name,
195
+ binary,
196
+ config_path,
197
+ status: 'running',
198
+ });
199
+ }
200
+ return { critters: discovered };
201
+ }
202
+ /**
203
+ * List systemd services on a barn
204
+ * @param barn - The barn to query
205
+ * @param activeOnly - If true, only return running services (default: true)
206
+ */
207
+ export async function listSystemServices(barn, activeOnly = true) {
208
+ // For active only: list-units shows running services
209
+ // For all: list-unit-files shows all installed services
210
+ const cmd = activeOnly
211
+ ? 'systemctl list-units --type=service --state=running --no-pager --no-legend'
212
+ : 'systemctl list-unit-files --type=service --no-pager --no-legend';
213
+ let output;
214
+ // Local barn
215
+ if (barn.name === 'local') {
216
+ try {
217
+ const result = await execa('sh', ['-c', cmd]);
218
+ output = result.stdout;
219
+ }
220
+ catch (err) {
221
+ return { services: [], error: `Failed to list services: ${getErrorMessage(err)}` };
222
+ }
223
+ }
224
+ else {
225
+ // Remote barn - SSH
226
+ if (!barn.host || !barn.user) {
227
+ return { services: [], error: `Barn '${barn.name}' is not configured for SSH` };
228
+ }
229
+ try {
230
+ const sshArgs = buildSshCommand(barn);
231
+ const result = await execa(sshArgs[0], [...sshArgs.slice(1), cmd]);
232
+ output = result.stdout;
233
+ }
234
+ catch (err) {
235
+ return { services: [], error: `SSH error: ${getErrorMessage(err)}` };
236
+ }
237
+ }
238
+ const services = [];
239
+ const lines = output.split('\n').filter(line => line.trim());
240
+ for (const line of lines) {
241
+ const parts = line.trim().split(/\s+/);
242
+ if (parts.length < 1)
243
+ continue;
244
+ const serviceName = parts[0];
245
+ if (!serviceName.endsWith('.service'))
246
+ continue;
247
+ if (activeOnly) {
248
+ // list-units format: UNIT LOAD ACTIVE SUB DESCRIPTION...
249
+ services.push({
250
+ name: serviceName,
251
+ state: 'running',
252
+ description: parts.slice(4).join(' ') || undefined,
253
+ });
254
+ }
255
+ else {
256
+ // list-unit-files format: UNIT STATE PRESET
257
+ const stateStr = parts[1]?.toLowerCase() || '';
258
+ services.push({
259
+ name: serviceName,
260
+ state: stateStr === 'enabled' ? 'unknown' : 'stopped',
261
+ });
262
+ }
263
+ }
264
+ return { services };
265
+ }
266
+ /**
267
+ * Get details about a systemd service by parsing its unit file
268
+ */
269
+ export async function getServiceDetails(barn, serviceName) {
270
+ // Get the unit file path
271
+ const pathCmd = `systemctl show -p FragmentPath ${shellEscape(serviceName)} --value`;
272
+ let servicePath;
273
+ if (barn.name === 'local') {
274
+ try {
275
+ const result = await execa('sh', ['-c', pathCmd]);
276
+ servicePath = result.stdout.trim();
277
+ }
278
+ catch (err) {
279
+ return { error: `Failed to get service path: ${getErrorMessage(err)}` };
280
+ }
281
+ }
282
+ else {
283
+ if (!barn.host || !barn.user) {
284
+ return { error: `Barn '${barn.name}' is not configured for SSH` };
285
+ }
286
+ try {
287
+ const sshArgs = buildSshCommand(barn);
288
+ const result = await execa(sshArgs[0], [...sshArgs.slice(1), pathCmd]);
289
+ servicePath = result.stdout.trim();
290
+ }
291
+ catch (err) {
292
+ return { error: `SSH error: ${getErrorMessage(err)}` };
293
+ }
294
+ }
295
+ if (!servicePath) {
296
+ return { error: 'Could not find service unit file' };
297
+ }
298
+ // Read the service file to extract details
299
+ const catCmd = `cat ${shellEscape(servicePath)}`;
300
+ let serviceContent;
301
+ if (barn.name === 'local') {
302
+ try {
303
+ const result = await execa('sh', ['-c', catCmd]);
304
+ serviceContent = result.stdout;
305
+ }
306
+ catch (err) {
307
+ return { error: `Failed to read service file: ${getErrorMessage(err)}` };
308
+ }
309
+ }
310
+ else {
311
+ try {
312
+ const sshArgs = buildSshCommand(barn);
313
+ const result = await execa(sshArgs[0], [...sshArgs.slice(1), catCmd]);
314
+ serviceContent = result.stdout;
315
+ }
316
+ catch (err) {
317
+ return { error: `SSH error: ${getErrorMessage(err)}` };
318
+ }
319
+ }
320
+ // Parse the service file
321
+ let config_path;
322
+ let log_path;
323
+ let use_journald = true;
324
+ for (const line of serviceContent.split('\n')) {
325
+ const trimmed = line.trim();
326
+ // Look for ExecStart to find config flags
327
+ if (trimmed.startsWith('ExecStart=')) {
328
+ const execLine = trimmed.slice('ExecStart='.length);
329
+ // Common config flag patterns
330
+ const configPatterns = [
331
+ /--config[=\s]([^\s]+)/,
332
+ /--defaults-file[=\s]([^\s]+)/,
333
+ /-c\s+([^\s]+)/,
334
+ /--conf[=\s]([^\s]+)/,
335
+ ];
336
+ for (const pattern of configPatterns) {
337
+ const match = execLine.match(pattern);
338
+ if (match) {
339
+ config_path = match[1];
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ // Check StandardOutput/StandardError for log paths
345
+ if (trimmed.startsWith('StandardOutput=') || trimmed.startsWith('StandardError=')) {
346
+ const value = trimmed.split('=')[1];
347
+ if (value && !value.startsWith('journal') && !value.startsWith('inherit')) {
348
+ // Could be file:path or append:path
349
+ const fileMatch = value.match(/(?:file|append):(.+)/);
350
+ if (fileMatch) {
351
+ log_path = fileMatch[1];
352
+ use_journald = false;
353
+ }
354
+ }
355
+ }
356
+ }
357
+ return {
358
+ details: {
359
+ service_path: servicePath,
360
+ config_path,
361
+ log_path,
362
+ use_journald,
363
+ },
364
+ };
365
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Install the Claude hook script to ~/.yeehaw/bin/
3
+ */
4
+ export declare function installHookScript(): string;
5
+ /**
6
+ * Get the path to the hook script
7
+ */
8
+ export declare function getHookScriptPath(): string;
9
+ /**
10
+ * Get the Claude settings.json hooks configuration
11
+ */
12
+ export declare function getClaudeHooksConfig(): object;
13
+ /**
14
+ * Check if Claude hooks are already configured
15
+ */
16
+ export declare function checkClaudeHooksInstalled(): boolean;
17
+ /**
18
+ * Check if hook script exists
19
+ */
20
+ export declare function hookScriptExists(): boolean;
@@ -0,0 +1,91 @@
1
+ import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { HOOKS_DIR, SIGNALS_DIR } from './paths.js';
5
+ const HOOK_SCRIPT_NAME = 'claude-hook';
6
+ const HOOK_SCRIPT_CONTENT = `#!/bin/bash
7
+ # Yeehaw Claude Hook - writes session status for the CLI to read
8
+ STATUS="$1"
9
+ PANE_ID="\${TMUX_PANE:-unknown}"
10
+ SIGNAL_DIR="$HOME/.yeehaw/session-signals"
11
+ SIGNAL_FILE="$SIGNAL_DIR/\${PANE_ID//[^a-zA-Z0-9]/_}.json"
12
+
13
+ mkdir -p "$SIGNAL_DIR"
14
+ cat > "$SIGNAL_FILE" << EOF
15
+ {"status":"$STATUS","updated":$(date +%s)}
16
+ EOF
17
+ `;
18
+ /**
19
+ * Install the Claude hook script to ~/.yeehaw/bin/
20
+ */
21
+ export function installHookScript() {
22
+ // Ensure directories exist
23
+ if (!existsSync(HOOKS_DIR)) {
24
+ mkdirSync(HOOKS_DIR, { recursive: true });
25
+ }
26
+ if (!existsSync(SIGNALS_DIR)) {
27
+ mkdirSync(SIGNALS_DIR, { recursive: true });
28
+ }
29
+ const scriptPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
30
+ writeFileSync(scriptPath, HOOK_SCRIPT_CONTENT, 'utf-8');
31
+ chmodSync(scriptPath, 0o755);
32
+ return scriptPath;
33
+ }
34
+ /**
35
+ * Get the path to the hook script
36
+ */
37
+ export function getHookScriptPath() {
38
+ return join(HOOKS_DIR, HOOK_SCRIPT_NAME);
39
+ }
40
+ /**
41
+ * Get the Claude settings.json hooks configuration
42
+ */
43
+ export function getClaudeHooksConfig() {
44
+ const hookPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
45
+ return {
46
+ hooks: {
47
+ PreToolUse: [
48
+ {
49
+ matcher: '*',
50
+ hooks: [`${hookPath} working`],
51
+ },
52
+ ],
53
+ Stop: [
54
+ {
55
+ matcher: '*',
56
+ hooks: [`${hookPath} waiting`],
57
+ },
58
+ ],
59
+ Notification: [
60
+ {
61
+ matcher: 'idle_prompt',
62
+ hooks: [`${hookPath} waiting`],
63
+ },
64
+ ],
65
+ },
66
+ };
67
+ }
68
+ /**
69
+ * Check if Claude hooks are already configured
70
+ */
71
+ export function checkClaudeHooksInstalled() {
72
+ const claudeSettingsPath = join(homedir(), '.claude', 'settings.json');
73
+ if (!existsSync(claudeSettingsPath)) {
74
+ return false;
75
+ }
76
+ try {
77
+ const content = readFileSync(claudeSettingsPath, 'utf-8');
78
+ const settings = JSON.parse(content);
79
+ return settings.hooks?.PreToolUse?.some((h) => h.hooks?.some((cmd) => cmd.includes('yeehaw')));
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Check if hook script exists
87
+ */
88
+ export function hookScriptExists() {
89
+ const scriptPath = join(HOOKS_DIR, HOOK_SCRIPT_NAME);
90
+ return existsSync(scriptPath);
91
+ }
@@ -1,4 +1,4 @@
1
- export type HotkeyScope = 'global' | 'global-dashboard' | 'project-context' | 'barn-context' | 'wiki-view' | 'issues-view' | 'livestock-detail' | 'logs-view' | 'night-sky' | 'list' | 'content';
1
+ export type HotkeyScope = 'global' | 'global-dashboard' | 'project-context' | 'barn-context' | 'wiki-view' | 'issues-view' | 'livestock-detail' | 'logs-view' | 'critter-detail' | 'critter-logs' | 'night-sky' | 'list' | 'content';
2
2
  export type HotkeyCategory = 'navigation' | 'action' | 'system';
3
3
  export interface Hotkey {
4
4
  key: string;
@@ -2,38 +2,46 @@
2
2
  export const HOTKEYS = [
3
3
  // === GLOBAL (everywhere) ===
4
4
  { key: '?', description: 'Toggle help', category: 'system', scopes: ['global'] },
5
- { key: 'q', description: 'Back / Detach', category: 'system', scopes: ['global'] },
5
+ { key: 'q', description: 'Detach', category: 'system', scopes: ['global-dashboard'] },
6
+ { key: 'Q', description: 'Quit Yeehaw', category: 'system', scopes: ['global-dashboard'] },
6
7
  { key: 'Esc', description: 'Back / Cancel', category: 'system', scopes: ['global'] },
7
8
  // === LIST NAVIGATION (any focused list) ===
8
- { key: 'j/k', description: 'Navigate up/down', category: 'navigation', scopes: ['list'] },
9
- { key: 'g/G', description: 'Go to first/last', category: 'navigation', scopes: ['list'] },
10
- { key: 'Enter', description: 'Select item', category: 'navigation', scopes: ['list'] },
9
+ { key: 'j/k', description: 'Move up/down', category: 'navigation', scopes: ['list'] },
10
+ { key: 'g/G', description: 'Jump to first/last', category: 'navigation', scopes: ['list'] },
11
+ { key: 'Enter', description: 'Open selected item', category: 'navigation', scopes: ['list'] },
11
12
  // === CONTENT NAVIGATION (markdown panels) ===
12
13
  { key: 'j/k', description: 'Scroll up/down', category: 'navigation', scopes: ['content'] },
13
14
  { key: 'g/G', description: 'Jump to top/bottom', category: 'navigation', scopes: ['content'] },
14
15
  { key: 'PgUp/PgDn', description: 'Scroll page', category: 'navigation', scopes: ['content'] },
15
- // === GLOBAL DASHBOARD ===
16
+ // === PANEL NAVIGATION ===
16
17
  { key: 'Tab', description: 'Switch panel', category: 'navigation', scopes: ['global-dashboard', 'project-context', 'barn-context', 'wiki-view', 'issues-view'] },
17
- { key: 'Q', description: 'Quit Yeehaw', category: 'system', scopes: ['global-dashboard'] },
18
- { key: 'c', description: 'New Claude session', category: 'action', scopes: ['global-dashboard', 'project-context'] },
19
- { key: '1-9', description: 'Quick switch window', category: 'action', scopes: ['global-dashboard', 'project-context'] },
20
- { key: 's', description: 'Open shell session', category: 'action', scopes: ['global-dashboard', 'project-context', 'barn-context', 'livestock-detail'], panel: 'barns' },
21
- // === CONTEXT-AWARE ACTIONS ===
18
+ // === GLOBAL SESSION SWITCHING ===
19
+ { key: '1-9', description: 'Switch to session', category: 'action', scopes: ['global-dashboard', 'project-context'] },
20
+ // === ROW-LEVEL ACTIONS (shown on selected rows) ===
21
+ { key: 'c', description: 'Claude session (at path)', category: 'action', scopes: ['global-dashboard'], panel: 'projects' },
22
+ { key: 'c', description: 'Claude session (at path)', category: 'action', scopes: ['project-context'], panel: 'livestock' },
23
+ { key: 's', description: 'Shell into server', category: 'action', scopes: ['global-dashboard'], panel: 'barns' },
24
+ { key: 's', description: 'Shell into server', category: 'action', scopes: ['project-context', 'barn-context'], panel: 'livestock' },
25
+ // === CARD/PAGE-LEVEL ACTIONS ===
22
26
  { key: 'n', description: 'New (in focused panel)', category: 'action', scopes: ['global-dashboard', 'project-context', 'barn-context', 'wiki-view'] },
23
- { key: 'e', description: 'Edit selected', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view'] },
24
- { key: 'd', description: 'Delete (with confirm)', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view'] },
25
- { key: 'D', description: 'Delete container (type name)', category: 'action', scopes: ['project-context', 'barn-context'] },
26
- // === PROJECT CONTEXT ===
27
+ { key: 'e', description: 'Edit', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view', 'livestock-detail'] },
28
+ { key: 'd', description: 'Delete', category: 'action', scopes: ['project-context', 'barn-context', 'wiki-view'] },
29
+ { key: 'D', description: 'Delete container', category: 'action', scopes: ['project-context', 'barn-context'] },
30
+ // === PROJECT CONTEXT PAGE-LEVEL ===
27
31
  { key: 'w', description: 'Open wiki', category: 'action', scopes: ['project-context'] },
28
32
  { key: 'i', description: 'Open issues', category: 'action', scopes: ['project-context'] },
33
+ // === LIVESTOCK DETAIL PAGE-LEVEL ===
34
+ { key: 'c', description: 'Claude session (local only)', category: 'action', scopes: ['livestock-detail'] },
35
+ { key: 's', description: 'Shell session', category: 'action', scopes: ['livestock-detail'] },
36
+ { key: 'l', description: 'View logs', category: 'action', scopes: ['livestock-detail'] },
37
+ // === CRITTER DETAIL PAGE-LEVEL ===
38
+ { key: 'l', description: 'View logs', category: 'action', scopes: ['critter-detail'] },
39
+ { key: 'e', description: 'Edit', category: 'action', scopes: ['critter-detail'] },
29
40
  // === ISSUES VIEW ===
30
- { key: 'r', description: 'Refresh issues', category: 'action', scopes: ['issues-view'] },
41
+ { key: 'r', description: 'Refresh', category: 'action', scopes: ['issues-view', 'logs-view', 'critter-logs'] },
31
42
  { key: 'o', description: 'Open in browser', category: 'action', scopes: ['issues-view'] },
32
- // === LIVESTOCK DETAIL ===
33
- { key: 'l', description: 'View logs', category: 'action', scopes: ['livestock-detail'] },
34
- // === LOGS VIEW ===
35
- { key: 'r', description: 'Refresh logs', category: 'action', scopes: ['logs-view'] },
36
- // === NIGHT SKY (screensaver) ===
43
+ { key: 'c', description: 'Open in Claude', category: 'action', scopes: ['issues-view'] },
44
+ // === NIGHT SKY ===
37
45
  { key: 'v', description: 'Visualizer', category: 'navigation', scopes: ['global-dashboard'] },
38
46
  { key: 'c', description: 'Spawn cloud', category: 'action', scopes: ['night-sky'] },
39
47
  { key: 'r', description: 'Randomize', category: 'action', scopes: ['night-sky'] },
@@ -0,0 +1,11 @@
1
+ import type { Issue, IssueProvider, FetchIssuesOptions } from './types.js';
2
+ import type { Livestock } from '../../types.js';
3
+ export declare class GitHubProvider implements IssueProvider {
4
+ readonly type: "github";
5
+ private repos;
6
+ constructor(livestock: Livestock[]);
7
+ isAuthenticated(): Promise<boolean>;
8
+ authenticate(): Promise<void>;
9
+ fetchIssues(options?: FetchIssuesOptions): Promise<Issue[]>;
10
+ getIssue(id: string): Promise<Issue>;
11
+ }