@colmbus72/yeehaw 0.5.0 → 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 (43) hide show
  1. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  2. package/dist/app.js +166 -15
  3. package/dist/components/CritterHeader.d.ts +7 -0
  4. package/dist/components/CritterHeader.js +81 -0
  5. package/dist/components/List.d.ts +2 -0
  6. package/dist/components/List.js +1 -1
  7. package/dist/lib/auth/index.d.ts +2 -0
  8. package/dist/lib/auth/index.js +3 -0
  9. package/dist/lib/auth/linear.d.ts +20 -0
  10. package/dist/lib/auth/linear.js +79 -0
  11. package/dist/lib/auth/storage.d.ts +12 -0
  12. package/dist/lib/auth/storage.js +53 -0
  13. package/dist/lib/context.d.ts +10 -0
  14. package/dist/lib/context.js +63 -0
  15. package/dist/lib/critters.d.ts +33 -0
  16. package/dist/lib/critters.js +164 -0
  17. package/dist/lib/hotkeys.d.ts +1 -1
  18. package/dist/lib/hotkeys.js +6 -2
  19. package/dist/lib/issues/github.d.ts +11 -0
  20. package/dist/lib/issues/github.js +154 -0
  21. package/dist/lib/issues/index.d.ts +14 -0
  22. package/dist/lib/issues/index.js +27 -0
  23. package/dist/lib/issues/linear.d.ts +24 -0
  24. package/dist/lib/issues/linear.js +345 -0
  25. package/dist/lib/issues/types.d.ts +82 -0
  26. package/dist/lib/issues/types.js +2 -0
  27. package/dist/lib/paths.d.ts +1 -0
  28. package/dist/lib/paths.js +1 -0
  29. package/dist/lib/tmux.d.ts +1 -0
  30. package/dist/lib/tmux.js +45 -0
  31. package/dist/types.d.ts +19 -0
  32. package/dist/views/BarnContext.d.ts +2 -1
  33. package/dist/views/BarnContext.js +136 -14
  34. package/dist/views/CritterDetailView.d.ts +10 -0
  35. package/dist/views/CritterDetailView.js +117 -0
  36. package/dist/views/CritterLogsView.d.ts +8 -0
  37. package/dist/views/CritterLogsView.js +100 -0
  38. package/dist/views/IssuesView.d.ts +2 -1
  39. package/dist/views/IssuesView.js +661 -98
  40. package/dist/views/LivestockDetailView.d.ts +2 -1
  41. package/dist/views/LivestockDetailView.js +8 -1
  42. package/dist/views/ProjectContext.js +35 -1
  43. package/package.json +1 -1
@@ -0,0 +1,345 @@
1
+ import { isLinearAuthenticated, linearGraphQL } from '../auth/index.js';
2
+ // GraphQL queries
3
+ const TEAMS_QUERY = `
4
+ query {
5
+ teams {
6
+ nodes {
7
+ id
8
+ name
9
+ key
10
+ }
11
+ }
12
+ }
13
+ `;
14
+ const VIEWER_QUERY = `
15
+ query {
16
+ viewer {
17
+ id
18
+ name
19
+ displayName
20
+ }
21
+ }
22
+ `;
23
+ const CYCLES_QUERY = `
24
+ query($teamId: String!) {
25
+ team(id: $teamId) {
26
+ cycles(orderBy: startsAt, first: 20) {
27
+ nodes {
28
+ id
29
+ name
30
+ number
31
+ startsAt
32
+ endsAt
33
+ }
34
+ }
35
+ activeCycle {
36
+ id
37
+ name
38
+ number
39
+ }
40
+ }
41
+ }
42
+ `;
43
+ const ASSIGNEES_QUERY = `
44
+ query($teamId: String!) {
45
+ team(id: $teamId) {
46
+ members {
47
+ nodes {
48
+ id
49
+ name
50
+ displayName
51
+ }
52
+ }
53
+ }
54
+ }
55
+ `;
56
+ const ISSUES_QUERY = `
57
+ query($teamId: String!, $first: Int, $filter: IssueFilter) {
58
+ team(id: $teamId) {
59
+ issues(first: $first, filter: $filter, orderBy: updatedAt) {
60
+ nodes {
61
+ id
62
+ identifier
63
+ title
64
+ description
65
+ url
66
+ createdAt
67
+ updatedAt
68
+ priority
69
+ estimate
70
+ state {
71
+ name
72
+ type
73
+ }
74
+ creator {
75
+ name
76
+ }
77
+ assignee {
78
+ id
79
+ name
80
+ displayName
81
+ }
82
+ cycle {
83
+ id
84
+ name
85
+ number
86
+ }
87
+ labels {
88
+ nodes {
89
+ name
90
+ }
91
+ }
92
+ comments {
93
+ nodes {
94
+ id
95
+ body
96
+ createdAt
97
+ user {
98
+ name
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ `;
107
+ const ISSUE_QUERY = `
108
+ query($id: String!) {
109
+ issue(id: $id) {
110
+ id
111
+ identifier
112
+ title
113
+ description
114
+ url
115
+ createdAt
116
+ updatedAt
117
+ priority
118
+ estimate
119
+ state {
120
+ name
121
+ type
122
+ }
123
+ creator {
124
+ name
125
+ }
126
+ assignee {
127
+ id
128
+ name
129
+ displayName
130
+ }
131
+ cycle {
132
+ id
133
+ name
134
+ number
135
+ }
136
+ labels {
137
+ nodes {
138
+ name
139
+ }
140
+ }
141
+ comments {
142
+ nodes {
143
+ id
144
+ body
145
+ createdAt
146
+ user {
147
+ name
148
+ }
149
+ }
150
+ }
151
+ team {
152
+ name
153
+ }
154
+ }
155
+ }
156
+ `;
157
+ export class LinearProvider {
158
+ type = 'linear';
159
+ teamId;
160
+ teamName;
161
+ cachedUserId;
162
+ cachedActiveCycleId;
163
+ constructor(teamId, teamName) {
164
+ this.teamId = teamId;
165
+ this.teamName = teamName;
166
+ }
167
+ async isAuthenticated() {
168
+ return isLinearAuthenticated();
169
+ }
170
+ async authenticate() {
171
+ throw new Error('Use saveLinearApiKey() to authenticate with a Personal API Key');
172
+ }
173
+ needsTeamSelection() {
174
+ return !this.teamId;
175
+ }
176
+ async fetchTeams() {
177
+ const data = await linearGraphQL(TEAMS_QUERY);
178
+ return data.teams.nodes;
179
+ }
180
+ setTeamId(teamId) {
181
+ this.teamId = teamId;
182
+ }
183
+ setTeamName(teamName) {
184
+ this.teamName = teamName;
185
+ }
186
+ getTeamName() {
187
+ return this.teamName;
188
+ }
189
+ async fetchTeamName() {
190
+ if (this.teamName)
191
+ return this.teamName;
192
+ if (!this.teamId)
193
+ return undefined;
194
+ try {
195
+ const data = await linearGraphQL(`query($teamId: String!) { team(id: $teamId) { name } }`, { teamId: this.teamId });
196
+ this.teamName = data.team.name;
197
+ return this.teamName;
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
202
+ }
203
+ async getCurrentUserId() {
204
+ if (this.cachedUserId !== undefined) {
205
+ return this.cachedUserId;
206
+ }
207
+ try {
208
+ const data = await linearGraphQL(VIEWER_QUERY);
209
+ this.cachedUserId = data.viewer.id;
210
+ return this.cachedUserId;
211
+ }
212
+ catch {
213
+ this.cachedUserId = null;
214
+ return null;
215
+ }
216
+ }
217
+ async fetchCycles() {
218
+ if (!this.teamId) {
219
+ throw new Error('Team not selected');
220
+ }
221
+ const data = await linearGraphQL(CYCLES_QUERY, { teamId: this.teamId });
222
+ // Cache the active cycle ID
223
+ if (data.team.activeCycle) {
224
+ this.cachedActiveCycleId = data.team.activeCycle.id;
225
+ }
226
+ return data.team.cycles.nodes.map((c) => ({
227
+ id: c.id,
228
+ name: c.name || `Cycle ${c.number}`,
229
+ number: c.number,
230
+ }));
231
+ }
232
+ getActiveCycleId() {
233
+ return this.cachedActiveCycleId;
234
+ }
235
+ async fetchAssignees() {
236
+ if (!this.teamId) {
237
+ throw new Error('Team not selected');
238
+ }
239
+ const data = await linearGraphQL(ASSIGNEES_QUERY, { teamId: this.teamId });
240
+ return data.team.members.nodes;
241
+ }
242
+ async fetchIssues(options = {}) {
243
+ if (!this.teamId) {
244
+ throw new Error('Team not selected');
245
+ }
246
+ const { state = 'open', limit = 50, linearFilter } = options;
247
+ // Build filter object
248
+ const filter = {};
249
+ // State filter
250
+ if (state === 'open') {
251
+ filter.state = { type: { in: ['backlog', 'unstarted', 'started'] } };
252
+ }
253
+ else if (state === 'closed') {
254
+ filter.state = { type: { in: ['completed', 'canceled'] } };
255
+ }
256
+ // Linear-specific filters
257
+ if (linearFilter) {
258
+ // Assignee filter
259
+ if (linearFilter.assigneeId !== undefined) {
260
+ if (linearFilter.assigneeId === null) {
261
+ filter.assignee = { null: true };
262
+ }
263
+ else {
264
+ filter.assignee = { id: { eq: linearFilter.assigneeId } };
265
+ }
266
+ }
267
+ // Cycle filter
268
+ if (linearFilter.cycleId) {
269
+ filter.cycle = { id: { eq: linearFilter.cycleId } };
270
+ }
271
+ // State type filter (overrides basic state filter)
272
+ if (linearFilter.stateType) {
273
+ const types = Array.isArray(linearFilter.stateType)
274
+ ? linearFilter.stateType
275
+ : [linearFilter.stateType];
276
+ filter.state = { type: { in: types } };
277
+ }
278
+ }
279
+ const data = await linearGraphQL(ISSUES_QUERY, { teamId: this.teamId, first: limit, filter: Object.keys(filter).length > 0 ? filter : undefined });
280
+ let issues = data.team.issues.nodes.map((issue) => this.normalizeIssue(issue));
281
+ // Client-side sorting
282
+ if (linearFilter?.sortBy === 'priority') {
283
+ // Priority: 1 = urgent (highest), 4 = low, 0 = no priority (lowest)
284
+ issues = issues.sort((a, b) => {
285
+ const aPri = a.priority === 0 ? 5 : (a.priority ?? 5);
286
+ const bPri = b.priority === 0 ? 5 : (b.priority ?? 5);
287
+ return linearFilter.sortDirection === 'desc' ? aPri - bPri : bPri - aPri;
288
+ });
289
+ }
290
+ else if (linearFilter?.sortBy === 'createdAt') {
291
+ issues = issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
292
+ }
293
+ // Default: already sorted by updatedAt from API
294
+ return issues;
295
+ }
296
+ async getIssue(id) {
297
+ const data = await linearGraphQL(ISSUE_QUERY, { id });
298
+ return this.normalizeIssue(data.issue);
299
+ }
300
+ normalizeIssue(issue) {
301
+ const openStateTypes = ['backlog', 'unstarted', 'started'];
302
+ const isOpen = openStateTypes.includes(issue.state.type);
303
+ const comments = issue.comments.nodes.map((c) => ({
304
+ id: c.id,
305
+ author: c.user?.name || 'Unknown',
306
+ body: c.body,
307
+ createdAt: c.createdAt,
308
+ }));
309
+ return {
310
+ id: issue.id,
311
+ identifier: issue.identifier,
312
+ title: issue.title,
313
+ state: issue.state.name,
314
+ stateType: issue.state.type,
315
+ isOpen,
316
+ author: issue.creator?.name || 'Unknown',
317
+ body: issue.description || '',
318
+ labels: issue.labels.nodes.map((l) => l.name),
319
+ url: issue.url,
320
+ createdAt: issue.createdAt,
321
+ updatedAt: issue.updatedAt,
322
+ comments,
323
+ source: {
324
+ type: 'linear',
325
+ team: issue.team?.name || this.teamName || 'Unknown',
326
+ },
327
+ priority: issue.priority,
328
+ estimate: issue.estimate ?? undefined,
329
+ assignee: issue.assignee
330
+ ? {
331
+ id: issue.assignee.id,
332
+ name: issue.assignee.name,
333
+ displayName: issue.assignee.displayName,
334
+ }
335
+ : undefined,
336
+ cycle: issue.cycle
337
+ ? {
338
+ id: issue.cycle.id,
339
+ name: issue.cycle.name || `Cycle ${issue.cycle.number}`,
340
+ number: issue.cycle.number,
341
+ }
342
+ : undefined,
343
+ };
344
+ }
345
+ }
@@ -0,0 +1,82 @@
1
+ export interface IssueComment {
2
+ id: string;
3
+ author: string;
4
+ body: string;
5
+ createdAt: string;
6
+ }
7
+ export type LinearPriority = 0 | 1 | 2 | 3 | 4;
8
+ export type LinearStateType = 'backlog' | 'unstarted' | 'started' | 'completed' | 'canceled' | 'triage';
9
+ export interface LinearAssignee {
10
+ id: string;
11
+ name: string;
12
+ displayName: string;
13
+ }
14
+ export interface LinearCycle {
15
+ id: string;
16
+ name: string;
17
+ number: number;
18
+ }
19
+ export interface Issue {
20
+ id: string;
21
+ identifier: string;
22
+ title: string;
23
+ state: string;
24
+ stateType?: LinearStateType;
25
+ isOpen: boolean;
26
+ author: string;
27
+ body: string;
28
+ labels: string[];
29
+ url: string;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ comments: IssueComment[];
33
+ source: IssueSource;
34
+ priority?: LinearPriority;
35
+ estimate?: number;
36
+ assignee?: LinearAssignee;
37
+ cycle?: LinearCycle;
38
+ }
39
+ export type IssueSource = {
40
+ type: 'github';
41
+ repo: string;
42
+ livestockName: string;
43
+ livestockPath: string;
44
+ } | {
45
+ type: 'linear';
46
+ team: string;
47
+ };
48
+ export interface LinearIssueFilter {
49
+ assigneeId?: string | null;
50
+ cycleId?: string;
51
+ stateType?: LinearStateType | LinearStateType[];
52
+ sortBy?: 'priority' | 'updatedAt' | 'createdAt';
53
+ sortDirection?: 'asc' | 'desc';
54
+ }
55
+ export interface FetchIssuesOptions {
56
+ state?: 'open' | 'closed' | 'all';
57
+ limit?: number;
58
+ linearFilter?: LinearIssueFilter;
59
+ }
60
+ export interface IssueProvider {
61
+ readonly type: 'github' | 'linear';
62
+ isAuthenticated(): Promise<boolean>;
63
+ authenticate(): Promise<void>;
64
+ fetchIssues(options?: FetchIssuesOptions): Promise<Issue[]>;
65
+ getIssue(id: string): Promise<Issue>;
66
+ }
67
+ export interface LinearProviderInterface extends IssueProvider {
68
+ readonly type: 'linear';
69
+ needsTeamSelection(): boolean;
70
+ fetchTeams(): Promise<LinearTeam[]>;
71
+ setTeamId(teamId: string): void;
72
+ setTeamName(teamName: string): void;
73
+ getTeamName(): string | undefined;
74
+ fetchCycles(): Promise<LinearCycle[]>;
75
+ fetchAssignees(): Promise<LinearAssignee[]>;
76
+ getCurrentUserId(): Promise<string | null>;
77
+ }
78
+ export interface LinearTeam {
79
+ id: string;
80
+ name: string;
81
+ key: string;
82
+ }
@@ -0,0 +1,2 @@
1
+ // src/lib/issues/types.ts
2
+ export {};
@@ -1,5 +1,6 @@
1
1
  export declare const YEEHAW_DIR: string;
2
2
  export declare const CONFIG_FILE: string;
3
+ export declare const AUTH_FILE: string;
3
4
  export declare const PROJECTS_DIR: string;
4
5
  export declare const BARNS_DIR: string;
5
6
  export declare const SESSIONS_DIR: string;
package/dist/lib/paths.js CHANGED
@@ -2,6 +2,7 @@ import { homedir } from 'os';
2
2
  import { join } from 'path';
3
3
  export const YEEHAW_DIR = join(homedir(), '.yeehaw');
4
4
  export const CONFIG_FILE = join(YEEHAW_DIR, 'config.yaml');
5
+ export const AUTH_FILE = join(YEEHAW_DIR, 'auth.yaml');
5
6
  export const PROJECTS_DIR = join(YEEHAW_DIR, 'projects');
6
7
  export const BARNS_DIR = join(YEEHAW_DIR, 'barns');
7
8
  export const SESSIONS_DIR = join(YEEHAW_DIR, 'sessions');
@@ -24,6 +24,7 @@ export declare function setupStatusBarHooks(): void;
24
24
  export declare function ensureCorrectStatusBar(): void;
25
25
  export declare function attachToYeehaw(): void;
26
26
  export declare function createClaudeWindow(workingDir: string, windowName: string): number;
27
+ export declare function createClaudeWindowWithPrompt(workingDir: string, windowName: string, systemPrompt: string): number;
27
28
  export declare function createShellWindow(workingDir: string, windowName: string, shell?: string): number;
28
29
  export declare function createSshWindow(windowName: string, host: string, user: string, port: number, identityFile: string, remotePath: string): number;
29
30
  export declare function detachFromSession(): void;
package/dist/lib/tmux.js CHANGED
@@ -131,19 +131,31 @@ export function attachToYeehaw() {
131
131
  }
132
132
  // All yeehaw MCP tools that should be auto-approved
133
133
  const YEEHAW_MCP_TOOLS = [
134
+ // Project management
134
135
  'mcp__yeehaw__list_projects',
135
136
  'mcp__yeehaw__get_project',
136
137
  'mcp__yeehaw__create_project',
137
138
  'mcp__yeehaw__update_project',
138
139
  'mcp__yeehaw__delete_project',
140
+ // Livestock management
139
141
  'mcp__yeehaw__add_livestock',
140
142
  'mcp__yeehaw__remove_livestock',
143
+ 'mcp__yeehaw__read_livestock_logs',
144
+ 'mcp__yeehaw__read_livestock_env',
145
+ // Barn management
141
146
  'mcp__yeehaw__list_barns',
142
147
  'mcp__yeehaw__get_barn',
143
148
  'mcp__yeehaw__create_barn',
144
149
  'mcp__yeehaw__update_barn',
145
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
146
157
  'mcp__yeehaw__get_wiki',
158
+ 'mcp__yeehaw__get_wiki_section',
147
159
  'mcp__yeehaw__add_wiki_section',
148
160
  'mcp__yeehaw__update_wiki_section',
149
161
  'mcp__yeehaw__delete_wiki_section',
@@ -190,6 +202,39 @@ export function createClaudeWindow(workingDir, windowName) {
190
202
  setWindowType(windowIndex, 'claude');
191
203
  return windowIndex;
192
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;
237
+ }
193
238
  export function createShellWindow(workingDir, windowName, shell) {
194
239
  // Use the user's configured shell from $SHELL, fallback to /bin/bash
195
240
  const userShell = shell || process.env.SHELL || '/bin/bash';
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;
@@ -41,6 +51,7 @@ export interface Livestock {
41
51
  export interface Critter {
42
52
  name: string;
43
53
  service: string;
54
+ service_path?: string;
44
55
  config_path?: string;
45
56
  log_path?: string;
46
57
  use_journald?: boolean;
@@ -92,6 +103,14 @@ export type AppView = {
92
103
  livestock: Livestock;
93
104
  source: 'project' | 'barn';
94
105
  sourceBarn?: Barn;
106
+ } | {
107
+ type: 'critter';
108
+ barn: Barn;
109
+ critter: Critter;
110
+ } | {
111
+ type: 'critter-logs';
112
+ barn: Barn;
113
+ critter: Critter;
95
114
  } | {
96
115
  type: 'night-sky';
97
116
  };
@@ -19,6 +19,7 @@ interface BarnContextProps {
19
19
  onRemoveLivestock: (project: Project, livestockName: string) => void;
20
20
  onAddCritter: (critter: Critter) => void;
21
21
  onRemoveCritter: (critterName: string) => void;
22
+ onSelectCritter: (critter: Critter) => void;
22
23
  }
23
- export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }: 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;
24
25
  export {};