@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
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
+ // Yeehaw brand gold (darker for light mode readability)
5
+ const BRAND_COLOR = '#d4a020';
4
6
  export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
5
7
  const [internalIndex, setInternalIndex] = useState(0);
6
8
  // Use controlled index if provided, otherwise internal
@@ -35,8 +37,13 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
35
37
  if (key.return && items[selectedIndex] && onSelect) {
36
38
  onSelect(items[selectedIndex]);
37
39
  }
38
- if (input === 's' && items[selectedIndex] && onAction) {
39
- onAction(items[selectedIndex]);
40
+ // Handle row-level actions (replaces hardcoded 's' check)
41
+ const currentItem = items[selectedIndex];
42
+ if (currentItem?.actions && onAction) {
43
+ const action = currentItem.actions.find(a => a.key === input);
44
+ if (action) {
45
+ onAction(currentItem, action.key);
46
+ }
40
47
  }
41
48
  });
42
49
  if (items.length === 0) {
@@ -44,10 +51,12 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
44
51
  }
45
52
  return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
46
53
  const isSelected = index === selectedIndex && focused;
54
+ // Session status takes priority for meta coloring
55
+ const sessionStatusColor = item.sessionStatus === 'waiting' ? 'yellow' :
56
+ item.sessionStatus === 'working' ? 'cyan' :
57
+ item.sessionStatus === 'error' ? 'red' : undefined;
47
58
  const statusColor = item.status === 'active' ? 'green' :
48
59
  item.status === 'error' ? 'red' : 'gray';
49
- // Yeehaw brand gold for selection
50
- const selectionColor = '#f0c040';
51
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isSelected ? selectionColor : undefined, children: isSelected ? '›' : ' ' }, "arrow"), _jsx(Text, { color: isSelected ? selectionColor : undefined, bold: isSelected, children: item.label }, "label"), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" }, "status")), item.meta && (_jsx(Text, { dimColor: true, children: item.meta }, "meta"))] }, item.id));
60
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [item.prefix && (_jsx(Box, { flexShrink: 0, children: item.prefix })), _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
52
61
  }) }));
53
62
  }
@@ -1,5 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ // Yeehaw brand gold (darker for light mode readability)
4
+ const BRAND_COLOR = '#d4a020';
5
+ // Render hints with gold keys and gray labels: "[n] new" -> gold "[n]" + gray " new"
6
+ function renderHints(hints) {
7
+ const parts = [];
8
+ // Match [key] patterns and split around them
9
+ const regex = /(\[[^\]]+\])/g;
10
+ let lastIndex = 0;
11
+ let match;
12
+ let keyIndex = 0;
13
+ while ((match = regex.exec(hints)) !== null) {
14
+ // Add text before the match (gray)
15
+ if (match.index > lastIndex) {
16
+ parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex, match.index) }, `text-${keyIndex}`));
17
+ }
18
+ // Add the [key] part (gold)
19
+ parts.push(_jsx(Text, { color: BRAND_COLOR, children: match[1] }, `key-${keyIndex}`));
20
+ lastIndex = regex.lastIndex;
21
+ keyIndex++;
22
+ }
23
+ // Add remaining text after last match (gray)
24
+ if (lastIndex < hints.length) {
25
+ parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex) }, `text-end`));
26
+ }
27
+ return parts;
28
+ }
3
29
  export function Panel({ title, children, focused = false, width, hints }) {
4
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', width: width, children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: focused ? 'cyan' : 'white', children: title }), focused && hints && (_jsx(Text, { color: "cyan", children: hints }))] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: children })] }));
30
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, children: [_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
5
31
  }
@@ -10,7 +10,7 @@ const TUMBLEWEED = [
10
10
  ' ░▒░ ░▒░ ░',
11
11
  ];
12
12
  const TUMBLEWEED_COLOR = '#b8860b';
13
- const BRAND_COLOR = '#f0c040';
13
+ const BRAND_COLOR = '#d4a020'; // Darker for light mode readability
14
14
  // Match Header.tsx positioning exactly
15
15
  const HEADER_PADDING_TOP = 1;
16
16
  const TUMBLEWEED_TOP_PADDING = 1;
@@ -18,8 +18,8 @@ export function useSessions() {
18
18
  const result = listYeehawWindows();
19
19
  setWindows(result);
20
20
  setLoading(false);
21
- // Poll every 5 seconds for window updates (without loading state changes)
22
- const interval = setInterval(refresh, 5000);
21
+ // Poll for window updates (without loading state changes)
22
+ const interval = setInterval(refresh, 800);
23
23
  return () => clearInterval(interval);
24
24
  }, [refresh]);
25
25
  const createClaude = useCallback((workingDir, name) => {
package/dist/index.js CHANGED
@@ -5,7 +5,43 @@ import { App } from './app.js';
5
5
  import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
6
6
  import { ensureConfigDirs } from './lib/config.js';
7
7
  import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
8
+ import { installHookScript, getClaudeHooksConfig, checkClaudeHooksInstalled } from './lib/hooks.js';
9
+ /**
10
+ * Handle CLI subcommands
11
+ */
12
+ function handleSubcommands() {
13
+ const args = process.argv.slice(2);
14
+ if (args[0] === 'hooks' && args[1] === 'install') {
15
+ const scriptPath = installHookScript();
16
+ console.log(`\x1b[32m✓\x1b[0m Hook script installed: ${scriptPath}`);
17
+ console.log('');
18
+ console.log('\x1b[33mNote:\x1b[0m Claude sessions started from Yeehaw already have hooks enabled.');
19
+ console.log('This command is only needed for Claude sessions started outside Yeehaw.');
20
+ if (checkClaudeHooksInstalled()) {
21
+ console.log('\n\x1b[32m✓\x1b[0m Claude hooks already configured in ~/.claude/settings.json');
22
+ }
23
+ else {
24
+ console.log('\nTo enable status tracking for external Claude sessions,');
25
+ console.log('add this to ~/.claude/settings.json:');
26
+ console.log(JSON.stringify(getClaudeHooksConfig(), null, 2));
27
+ }
28
+ return true;
29
+ }
30
+ if (args[0] === 'hooks') {
31
+ console.log('Usage: yeehaw hooks install');
32
+ console.log('');
33
+ console.log('Install Claude hooks for session status tracking.');
34
+ console.log('Note: Sessions started from Yeehaw already have hooks enabled automatically.');
35
+ console.log('This is only needed for Claude sessions started outside Yeehaw.');
36
+ return true;
37
+ }
38
+ return false;
39
+ }
8
40
  function main() {
41
+ // Handle subcommands first (before tmux checks)
42
+ if (handleSubcommands()) {
43
+ process.exit(0);
44
+ }
9
45
  // Ensure config directories exist
10
46
  ensureConfigDirs();
11
47
  // Check for updates (non-blocking, uses cache)
@@ -27,7 +63,11 @@ function main() {
27
63
  }
28
64
  // If we're already inside the yeehaw tmux session, just render the TUI
29
65
  if (isInsideYeehawSession()) {
30
- render(_jsx(App, {}));
66
+ render(_jsx(App, {}), {
67
+ patchConsole: true,
68
+ incrementalRendering: true,
69
+ // maxFps: 60,
70
+ });
31
71
  return;
32
72
  }
33
73
  // We're not inside yeehaw session - need to create/attach
@@ -0,0 +1,2 @@
1
+ export * from './storage.js';
2
+ export * from './linear.js';
@@ -0,0 +1,3 @@
1
+ // src/lib/auth/index.ts
2
+ export * from './storage.js';
3
+ export * from './linear.js';
@@ -0,0 +1,20 @@
1
+ import { clearLinearToken } from './storage.js';
2
+ export declare const LINEAR_API_KEY_URL = "https://linear.app/settings/api";
3
+ export { clearLinearToken };
4
+ /**
5
+ * Check if Linear is currently authenticated.
6
+ */
7
+ export declare function isLinearAuthenticated(): boolean;
8
+ /**
9
+ * Save a Linear API key.
10
+ */
11
+ export declare function saveLinearApiKey(apiKey: string): void;
12
+ /**
13
+ * Validate a Linear API key by making a test request.
14
+ * Returns true if valid, false otherwise.
15
+ */
16
+ export declare function validateLinearApiKey(apiKey: string): Promise<boolean>;
17
+ /**
18
+ * Make an authenticated request to Linear's GraphQL API.
19
+ */
20
+ export declare function linearGraphQL<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
@@ -0,0 +1,79 @@
1
+ // src/lib/auth/linear.ts
2
+ // Linear authentication using Personal API Keys
3
+ // Users create keys at: https://linear.app/settings/api
4
+ import { setLinearToken, getLinearToken, clearLinearToken } from './storage.js';
5
+ export const LINEAR_API_KEY_URL = 'https://linear.app/settings/api';
6
+ export { clearLinearToken };
7
+ /**
8
+ * Check if Linear is currently authenticated.
9
+ */
10
+ export function isLinearAuthenticated() {
11
+ return getLinearToken() !== null;
12
+ }
13
+ /**
14
+ * Save a Linear API key.
15
+ */
16
+ export function saveLinearApiKey(apiKey) {
17
+ setLinearToken(apiKey);
18
+ }
19
+ /**
20
+ * Validate a Linear API key by making a test request.
21
+ * Returns true if valid, false otherwise.
22
+ */
23
+ export async function validateLinearApiKey(apiKey) {
24
+ try {
25
+ const response = await fetch('https://api.linear.app/graphql', {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'Authorization': apiKey,
30
+ },
31
+ body: JSON.stringify({
32
+ query: '{ viewer { id } }',
33
+ }),
34
+ });
35
+ if (!response.ok) {
36
+ return false;
37
+ }
38
+ const text = await response.text();
39
+ // Check if we got HTML instead of JSON (indicates auth/routing issue)
40
+ if (text.startsWith('<!') || text.startsWith('<html')) {
41
+ return false;
42
+ }
43
+ const result = JSON.parse(text);
44
+ return !!(result.data?.viewer?.id);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ /**
51
+ * Make an authenticated request to Linear's GraphQL API.
52
+ */
53
+ export async function linearGraphQL(query, variables) {
54
+ const token = getLinearToken();
55
+ if (!token) {
56
+ throw new Error('Not authenticated with Linear');
57
+ }
58
+ const response = await fetch('https://api.linear.app/graphql', {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'Authorization': token,
63
+ },
64
+ body: JSON.stringify({ query, variables }),
65
+ });
66
+ if (!response.ok) {
67
+ throw new Error(`Linear API error: ${response.statusText}`);
68
+ }
69
+ const text = await response.text();
70
+ // Check if we got HTML instead of JSON
71
+ if (text.startsWith('<!') || text.startsWith('<html')) {
72
+ throw new Error('Linear API returned HTML instead of JSON - check your API key');
73
+ }
74
+ const result = JSON.parse(text);
75
+ if (result.errors?.length) {
76
+ throw new Error(result.errors[0].message);
77
+ }
78
+ return result.data;
79
+ }
@@ -0,0 +1,12 @@
1
+ export interface LinearAuth {
2
+ accessToken: string;
3
+ expiresAt?: string;
4
+ }
5
+ export interface AuthConfig {
6
+ linear?: LinearAuth;
7
+ }
8
+ export declare function loadAuth(): AuthConfig;
9
+ export declare function saveAuth(auth: AuthConfig): void;
10
+ export declare function getLinearToken(): string | null;
11
+ export declare function setLinearToken(accessToken: string, expiresAt?: Date): void;
12
+ export declare function clearLinearToken(): void;
@@ -0,0 +1,53 @@
1
+ // src/lib/auth/storage.ts
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import YAML from 'js-yaml';
4
+ import { AUTH_FILE, YEEHAW_DIR } from '../paths.js';
5
+ import { mkdirSync } from 'fs';
6
+ export function loadAuth() {
7
+ if (!existsSync(YEEHAW_DIR)) {
8
+ mkdirSync(YEEHAW_DIR, { recursive: true });
9
+ }
10
+ if (!existsSync(AUTH_FILE)) {
11
+ return {};
12
+ }
13
+ try {
14
+ const content = readFileSync(AUTH_FILE, 'utf-8');
15
+ return YAML.load(content) || {};
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export function saveAuth(auth) {
22
+ if (!existsSync(YEEHAW_DIR)) {
23
+ mkdirSync(YEEHAW_DIR, { recursive: true });
24
+ }
25
+ writeFileSync(AUTH_FILE, YAML.dump(auth), 'utf-8');
26
+ }
27
+ export function getLinearToken() {
28
+ const auth = loadAuth();
29
+ if (!auth.linear?.accessToken) {
30
+ return null;
31
+ }
32
+ // Check expiration if set
33
+ if (auth.linear.expiresAt) {
34
+ const expiresAt = new Date(auth.linear.expiresAt);
35
+ if (expiresAt <= new Date()) {
36
+ return null; // Token expired
37
+ }
38
+ }
39
+ return auth.linear.accessToken;
40
+ }
41
+ export function setLinearToken(accessToken, expiresAt) {
42
+ const auth = loadAuth();
43
+ auth.linear = {
44
+ accessToken,
45
+ expiresAt: expiresAt?.toISOString(),
46
+ };
47
+ saveAuth(auth);
48
+ }
49
+ export function clearLinearToken() {
50
+ const auth = loadAuth();
51
+ delete auth.linear;
52
+ saveAuth(auth);
53
+ }
@@ -1,4 +1,4 @@
1
- import type { Config, Project, Barn, Livestock } from '../types.js';
1
+ import type { Config, Project, Barn, Livestock, Critter } from '../types.js';
2
2
  export declare const LOCAL_BARN: Barn;
3
3
  export declare function isLocalBarn(barn: Barn): boolean;
4
4
  /**
@@ -25,3 +25,15 @@ export declare function getLivestockForBarn(barnName: string): Array<{
25
25
  project: Project;
26
26
  livestock: Livestock;
27
27
  }>;
28
+ /**
29
+ * Add a critter to a barn
30
+ */
31
+ export declare function addCritterToBarn(barnName: string, critter: Critter): void;
32
+ /**
33
+ * Remove a critter from a barn
34
+ */
35
+ export declare function removeCritterFromBarn(barnName: string, critterName: string): boolean;
36
+ /**
37
+ * Get a specific critter from a barn
38
+ */
39
+ export declare function getCritter(barnName: string, critterName: string): Critter | undefined;
@@ -148,3 +148,54 @@ export function getLivestockForBarn(barnName) {
148
148
  }
149
149
  return result;
150
150
  }
151
+ // ============================================================================
152
+ // Critter operations
153
+ // ============================================================================
154
+ /**
155
+ * Add a critter to a barn
156
+ */
157
+ export function addCritterToBarn(barnName, critter) {
158
+ const barn = loadBarn(barnName);
159
+ if (!barn) {
160
+ throw new Error(`Barn not found: ${barnName}`);
161
+ }
162
+ barn.critters = barn.critters || [];
163
+ // Check for duplicate
164
+ if (barn.critters.some(c => c.name === critter.name)) {
165
+ throw new Error(`Critter "${critter.name}" already exists on barn "${barnName}"`);
166
+ }
167
+ barn.critters.push(critter);
168
+ // Only save to file if it's not the local barn
169
+ if (barnName !== 'local') {
170
+ saveBarn(barn);
171
+ }
172
+ }
173
+ /**
174
+ * Remove a critter from a barn
175
+ */
176
+ export function removeCritterFromBarn(barnName, critterName) {
177
+ const barn = loadBarn(barnName);
178
+ if (!barn) {
179
+ throw new Error(`Barn not found: ${barnName}`);
180
+ }
181
+ const originalLength = (barn.critters || []).length;
182
+ barn.critters = (barn.critters || []).filter(c => c.name !== critterName);
183
+ if (barn.critters.length === originalLength) {
184
+ return false; // Critter wasn't found
185
+ }
186
+ // Only save to file if it's not the local barn
187
+ if (barnName !== 'local') {
188
+ saveBarn(barn);
189
+ }
190
+ return true;
191
+ }
192
+ /**
193
+ * Get a specific critter from a barn
194
+ */
195
+ export function getCritter(barnName, critterName) {
196
+ const barn = loadBarn(barnName);
197
+ if (!barn) {
198
+ return undefined;
199
+ }
200
+ return barn.critters?.find(c => c.name === critterName);
201
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Build a context string to inject into Claude sessions spawned from Yeehaw.
3
+ * Includes project name and wiki section titles (not content) to hint at available context.
4
+ */
5
+ export declare function buildProjectContext(projectName: string): string | null;
6
+ /**
7
+ * Build context for a livestock-specific session.
8
+ * Includes project context plus livestock details.
9
+ */
10
+ export declare function buildLivestockContext(projectName: string, livestockName: string): string | null;
@@ -0,0 +1,63 @@
1
+ import { loadProject } from './config.js';
2
+ /**
3
+ * Build a context string to inject into Claude sessions spawned from Yeehaw.
4
+ * Includes project name and wiki section titles (not content) to hint at available context.
5
+ */
6
+ export function buildProjectContext(projectName) {
7
+ const project = loadProject(projectName);
8
+ if (!project) {
9
+ return null;
10
+ }
11
+ const lines = [];
12
+ lines.push(`You are working on the "${project.name}" project.`);
13
+ if (project.summary) {
14
+ lines.push(`Project: ${project.summary}`);
15
+ }
16
+ // Add wiki section titles as hints
17
+ const wikiSections = project.wiki || [];
18
+ if (wikiSections.length > 0) {
19
+ lines.push('');
20
+ lines.push('Yeehaw wiki sections available:');
21
+ for (const section of wikiSections) {
22
+ lines.push(`- ${section.title}`);
23
+ }
24
+ lines.push('');
25
+ lines.push('Use mcp__yeehaw__get_wiki_section to fetch relevant context before making architectural decisions.');
26
+ }
27
+ return lines.join('\n');
28
+ }
29
+ /**
30
+ * Build context for a livestock-specific session.
31
+ * Includes project context plus livestock details.
32
+ */
33
+ export function buildLivestockContext(projectName, livestockName) {
34
+ const project = loadProject(projectName);
35
+ if (!project) {
36
+ return null;
37
+ }
38
+ const livestock = project.livestock?.find(l => l.name === livestockName);
39
+ if (!livestock) {
40
+ return buildProjectContext(projectName);
41
+ }
42
+ const lines = [];
43
+ lines.push(`You are working on the "${project.name}" project, in the "${livestock.name}" environment.`);
44
+ if (project.summary) {
45
+ lines.push(`Project: ${project.summary}`);
46
+ }
47
+ // Add livestock details
48
+ if (livestock.branch) {
49
+ lines.push(`Branch: ${livestock.branch}`);
50
+ }
51
+ // Add wiki section titles as hints
52
+ const wikiSections = project.wiki || [];
53
+ if (wikiSections.length > 0) {
54
+ lines.push('');
55
+ lines.push('Yeehaw wiki sections available:');
56
+ for (const section of wikiSections) {
57
+ lines.push(`- ${section.title}`);
58
+ }
59
+ lines.push('');
60
+ lines.push('Use mcp__yeehaw__get_wiki_section to fetch relevant context before making architectural decisions.');
61
+ }
62
+ return lines.join('\n');
63
+ }
@@ -0,0 +1,61 @@
1
+ import type { Critter, Barn } from '../types.js';
2
+ /**
3
+ * Discovered critter from scanning a barn
4
+ */
5
+ export interface DiscoveredCritter {
6
+ service: string;
7
+ suggested_name: string;
8
+ binary?: string;
9
+ config_path?: string;
10
+ status: 'running' | 'stopped';
11
+ }
12
+ /**
13
+ * Read logs from a critter (via journald or custom path)
14
+ */
15
+ export declare function readCritterLogs(critter: Critter, barn: Barn, options?: {
16
+ lines?: number;
17
+ pattern?: string;
18
+ }): Promise<{
19
+ content: string;
20
+ error?: string;
21
+ }>;
22
+ /**
23
+ * Discover critters (running services) on a barn
24
+ */
25
+ export declare function discoverCritters(barn: Barn): Promise<{
26
+ critters: DiscoveredCritter[];
27
+ error?: string;
28
+ }>;
29
+ /**
30
+ * Service info from systemctl
31
+ */
32
+ export interface SystemService {
33
+ name: string;
34
+ state: 'running' | 'stopped' | 'unknown';
35
+ description?: string;
36
+ }
37
+ /**
38
+ * List systemd services on a barn
39
+ * @param barn - The barn to query
40
+ * @param activeOnly - If true, only return running services (default: true)
41
+ */
42
+ export declare function listSystemServices(barn: Barn, activeOnly?: boolean): Promise<{
43
+ services: SystemService[];
44
+ error?: string;
45
+ }>;
46
+ /**
47
+ * Details extracted from a systemd service file
48
+ */
49
+ export interface ServiceDetails {
50
+ service_path: string;
51
+ config_path?: string;
52
+ log_path?: string;
53
+ use_journald: boolean;
54
+ }
55
+ /**
56
+ * Get details about a systemd service by parsing its unit file
57
+ */
58
+ export declare function getServiceDetails(barn: Barn, serviceName: string): Promise<{
59
+ details?: ServiceDetails;
60
+ error?: string;
61
+ }>;