@hailer/mcp 0.0.4 → 0.0.6

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.
@@ -7,6 +7,35 @@ description: Complete guide for building Hailer apps with the @hailer/app-sdk -
7
7
 
8
8
  Complete guide for building Hailer apps using the `@hailer/app-sdk` package. This skill covers critical patterns and gotchas that prevent data loading issues.
9
9
 
10
+ ---
11
+
12
+ ## ⚠️ CRITICAL FIRST STEP: Fix main.tsx with ChakraProvider
13
+
14
+ **THE SCAFFOLD TEMPLATE IS MISSING ChakraProvider!** You MUST fix this FIRST or styles won't render:
15
+
16
+ ```typescript
17
+ // src/main.tsx - REPLACE the entire file with this:
18
+ import React from 'react'
19
+ import ReactDOM from 'react-dom/client'
20
+ import { ChakraProvider } from '@chakra-ui/react'
21
+ import App from './App.tsx'
22
+ import './index.css'
23
+
24
+ ReactDOM.createRoot(document.getElementById('root')!).render(
25
+ <React.StrictMode>
26
+ <ChakraProvider>
27
+ <App />
28
+ </ChakraProvider>
29
+ </React.StrictMode>,
30
+ )
31
+ ```
32
+
33
+ **Symptom if missing:** App renders with plain HTML, no styling, no colors, no layout.
34
+
35
+ **Fix:** Wrap `<App />` with `<ChakraProvider>`.
36
+
37
+ ---
38
+
10
39
  ## ⚠️ CRITICAL: Hailer App SDK vs MCP API
11
40
 
12
41
  **IMPORTANT:** The Hailer App SDK (used in React/frontend apps) is DIFFERENT from the MCP API (used by Claude Code tools).
@@ -94,7 +123,133 @@ hailer.activity.list(
94
123
 
95
124
  ---
96
125
 
97
- ## 2. Field Access - The Critical Issue
126
+ ## 2. Loading Workflow Schema (Field Definitions)
127
+
128
+ ### ⚠️ CRITICAL: No `hailer.process.getFields()` - Use `hailer.workflow.get()`
129
+
130
+ The SDK does NOT have `hailer.process.getFields()`. This is a common mistake!
131
+
132
+ ### ✅ CORRECT: Load Schema with `hailer.workflow.get()`
133
+
134
+ ```typescript
135
+ // Load workflow with all field definitions
136
+ const workflow = await hailer.workflow.get(workflowId);
137
+
138
+ // workflow.fields is an object keyed by field ID:
139
+ // {
140
+ // "691ffdf84217e9e8434e5694": { label: "Player Name", type: "text", key: "playerName", ... },
141
+ // "691ffdf84217e9e8434e5695": { label: "Jersey Number", type: "numeric", key: "jerseyNumber", ... }
142
+ // }
143
+
144
+ // workflow.phases is an object with phase-specific field lists:
145
+ // {
146
+ // "691ffdf84217e9e8434e569f": { name: "Active", fields: ["field1", "field2", ...] }
147
+ // }
148
+ ```
149
+
150
+ ### Complete Schema Loading Pattern
151
+
152
+ ```typescript
153
+ import { useState, useEffect, useCallback } from 'react';
154
+
155
+ interface FieldSchema {
156
+ _id: string;
157
+ key?: string;
158
+ label: string;
159
+ type: string;
160
+ required?: boolean;
161
+ data?: string[]; // For dropdown options
162
+ }
163
+
164
+ function WorkflowView({ hailer, workflowId, phaseId }) {
165
+ const [schema, setSchema] = useState<FieldSchema[]>([]);
166
+
167
+ const loadSchema = useCallback(async () => {
168
+ try {
169
+ // ✅ CORRECT: Use hailer.workflow.get()
170
+ const workflowData = await hailer.workflow.get(workflowId);
171
+
172
+ if (workflowData?.fields) {
173
+ // Get field IDs for this phase, or all fields if phase not specified
174
+ const fieldIds = workflowData.phases?.[phaseId]?.fields
175
+ || Object.keys(workflowData.fields);
176
+
177
+ // Convert fields object to array
178
+ const fieldsArray = fieldIds.map((fieldId: string) => {
179
+ const field = workflowData.fields[fieldId];
180
+ return {
181
+ _id: fieldId,
182
+ key: field?.key,
183
+ label: field?.label || fieldId,
184
+ type: field?.type || 'text',
185
+ required: field?.required,
186
+ data: field?.data, // Dropdown options
187
+ };
188
+ }).filter(f => f.label);
189
+
190
+ setSchema(fieldsArray);
191
+ }
192
+ } catch (err) {
193
+ console.error('Failed to load schema:', err);
194
+ }
195
+ }, [hailer, workflowId, phaseId]);
196
+
197
+ useEffect(() => {
198
+ loadSchema();
199
+ }, [loadSchema]);
200
+
201
+ // ... rest of component
202
+ }
203
+ ```
204
+
205
+ ### ❌ WRONG: These Don't Exist
206
+
207
+ ```typescript
208
+ // ❌ WRONG: hailer.process.getFields() doesn't exist!
209
+ await hailer.process.getFields(workflowId, phaseId);
210
+
211
+ // ❌ WRONG: hailer.workflow.getFields() doesn't exist!
212
+ await hailer.workflow.getFields(workflowId);
213
+
214
+ // ❌ WRONG: hailer.workflow.getSchema() doesn't exist!
215
+ await hailer.workflow.getSchema(workflowId);
216
+ ```
217
+
218
+ ### SDK Workflow API Reference
219
+
220
+ ```typescript
221
+ // Available workflow methods:
222
+ hailer.workflow.list() // List all workflows
223
+ hailer.workflow.get(workflowId: string) // Get single workflow with fields
224
+
225
+ // Workflow object structure:
226
+ interface Workflow {
227
+ _id: string;
228
+ name: string;
229
+ fields: {
230
+ [fieldId: string]: {
231
+ _id: string;
232
+ label: string;
233
+ type: string;
234
+ key?: string;
235
+ required?: boolean;
236
+ data?: string[]; // For textpredefinedoptions
237
+ };
238
+ };
239
+ phases: {
240
+ [phaseId: string]: {
241
+ _id: string;
242
+ name: string;
243
+ fields?: string[]; // Field IDs visible in this phase
244
+ };
245
+ };
246
+ phasesOrder: string[];
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 3. Field Access - The Critical Issue
98
253
 
99
254
  ### Understanding Field Structure
100
255
 
@@ -186,7 +341,7 @@ function PlayerCard({ player }) {
186
341
 
187
342
  ---
188
343
 
189
- ## 3. Complete Working Example
344
+ ## 4. Complete Working Example
190
345
 
191
346
  ### Full Component with Proper Data Loading
192
347
 
@@ -256,7 +411,7 @@ export default function PlayerList({ hailer }) {
256
411
 
257
412
  ---
258
413
 
259
- ## 4. Common Field Types
414
+ ## 5. Common Field Types
260
415
 
261
416
  ### Field Type Reference
262
417
 
@@ -307,7 +462,7 @@ console.log(team?.name);
307
462
 
308
463
  ---
309
464
 
310
- ## 5. Development Workflow
465
+ ## 6. Development Workflow
311
466
 
312
467
  ### Step-by-Step: Building a Hailer App Component
313
468
 
@@ -347,7 +502,7 @@ console.log(team?.name);
347
502
 
348
503
  ---
349
504
 
350
- ## 6. Debugging Data Loading Issues
505
+ ## 7. Debugging Data Loading Issues
351
506
 
352
507
  ### Quick Diagnostics
353
508
 
@@ -383,13 +538,14 @@ async function loadPlayers() {
383
538
  | Issue | Symptom | Solution |
384
539
  |-------|---------|----------|
385
540
  | `Cannot read properties of undefined (reading 'activity')` | `hailer.api` doesn't exist | Use `hailer.activity`, NOT `hailer.api.activity` |
541
+ | `hailer.process.getFields is not a function` | Method doesn't exist | Use `hailer.workflow.get(workflowId)` to get fields |
386
542
  | `"processId" must be a string` | Passing object instead of positional params | Use 3 positional params: `list(workflowId, phaseId, options)` |
387
543
  | Fields showing as `undefined` | Using readable keys like `fields.playerName` | Use field IDs: `fields['691ffdf...']` or `getFieldValue()` |
388
544
  | Empty data displayed | Data loads but doesn't render | Check field access - use correct field IDs |
389
545
 
390
546
  ---
391
547
 
392
- ## 7. Loading Data from Multiple Phases
548
+ ## 8. Loading Data from Multiple Phases
393
549
 
394
550
  ```typescript
395
551
  async function loadAllPlayers() {
@@ -426,7 +582,116 @@ async function loadAllPlayers() {
426
582
 
427
583
  ---
428
584
 
429
- ## 8. Best Practices
585
+ ## 8b. Loading Activity Counts for Sidebar Navigation
586
+
587
+ When building a data manager app with a sidebar showing all workflows, you want to display activity counts immediately without requiring the user to click each workflow first.
588
+
589
+ ### ✅ CORRECT: Load counts on app initialization
590
+
591
+ ```typescript
592
+ import { useState, useCallback, useEffect } from 'react';
593
+
594
+ export default function App() {
595
+ const { hailer, inside } = useHailer();
596
+ const [activityCounts, setActivityCounts] = useState<Record<string, number>>({});
597
+ const [countsLoaded, setCountsLoaded] = useState(false);
598
+
599
+ // Load activity counts for all workflows on initial load
600
+ useEffect(() => {
601
+ if (!hailer || !inside || countsLoaded) return;
602
+
603
+ async function loadWorkflowCount(workflowId: string): Promise<[string, number]> {
604
+ try {
605
+ // Get workflow to find all phases
606
+ const workflowData = await hailer.workflow.get(workflowId);
607
+ if (!workflowData?.phases) return [workflowId, 0];
608
+
609
+ // Count activities across all phases in parallel
610
+ const phaseIds = Object.keys(workflowData.phases);
611
+ const phaseCounts = await Promise.all(
612
+ phaseIds.map(async (phaseId) => {
613
+ try {
614
+ const activities = await hailer.activity.list(workflowId, phaseId, { limit: 500 });
615
+ return activities?.length || 0;
616
+ } catch {
617
+ return 0;
618
+ }
619
+ })
620
+ );
621
+
622
+ const totalCount = phaseCounts.reduce((sum, count) => sum + count, 0);
623
+ return [workflowId, totalCount];
624
+ } catch {
625
+ return [workflowId, 0];
626
+ }
627
+ }
628
+
629
+ async function loadAllCounts() {
630
+ // Load all workflow counts in parallel
631
+ const workflows = Object.values(WORKFLOWS);
632
+ const results = await Promise.all(
633
+ workflows.map((workflow) => loadWorkflowCount(workflow.id))
634
+ );
635
+
636
+ const counts: Record<string, number> = {};
637
+ for (const [workflowId, count] of results) {
638
+ counts[workflowId] = count;
639
+ }
640
+
641
+ setActivityCounts(counts);
642
+ setCountsLoaded(true);
643
+ }
644
+
645
+ loadAllCounts();
646
+ }, [hailer, inside, countsLoaded]);
647
+
648
+ // ... rest of component
649
+ }
650
+ ```
651
+
652
+ ### Sidebar Component with Counts
653
+
654
+ ```typescript
655
+ interface SidebarProps {
656
+ selectedWorkflow: WorkflowKey | null;
657
+ onSelectWorkflow: (key: WorkflowKey) => void;
658
+ activityCounts: Record<string, number>;
659
+ }
660
+
661
+ export default function Sidebar({ selectedWorkflow, onSelectWorkflow, activityCounts }: SidebarProps) {
662
+ return (
663
+ <aside className="sidebar">
664
+ <nav className="sidebar-nav">
665
+ {Object.entries(WORKFLOWS).map(([key, workflow]) => (
666
+ <button
667
+ key={key}
668
+ className={`nav-item ${selectedWorkflow === key ? 'active' : ''}`}
669
+ onClick={() => onSelectWorkflow(key as WorkflowKey)}
670
+ >
671
+ <span className="nav-icon">{workflow.icon}</span>
672
+ <span className="nav-label">{workflow.name}</span>
673
+ {activityCounts[workflow.id] !== undefined && (
674
+ <span className="nav-count">{activityCounts[workflow.id]}</span>
675
+ )}
676
+ </button>
677
+ ))}
678
+ </nav>
679
+ </aside>
680
+ );
681
+ }
682
+ ```
683
+
684
+ ### Key Points
685
+
686
+ 1. **Load on initialization** - Use `useEffect` to load counts when `hailer` and `inside` are ready
687
+ 2. **Parallel loading** - Use `Promise.all` to load all workflows simultaneously
688
+ 3. **Count all phases** - A workflow may have activities in multiple phases
689
+ 4. **Show in sidebar** - Display counts next to workflow names
690
+ 5. **Update on changes** - Optionally refresh counts when activities are created/updated
691
+
692
+ ---
693
+
694
+ ## 9. Best Practices
430
695
 
431
696
  ### ✅ DO
432
697
 
@@ -448,7 +713,7 @@ async function loadAllPlayers() {
448
713
 
449
714
  ---
450
715
 
451
- ## 9. TypeScript Definitions
716
+ ## 10. TypeScript Definitions
452
717
 
453
718
  ```typescript
454
719
  // src/types.ts
@@ -502,7 +767,7 @@ export interface HailerClient {
502
767
 
503
768
  ---
504
769
 
505
- ## 10. Testing Checklist
770
+ ## 11. Testing Checklist
506
771
 
507
772
  Before deploying your Hailer app:
508
773
 
@@ -0,0 +1,340 @@
1
+ ---
2
+ name: Hailer App Builder
3
+ description: TypeScript standards and clean code patterns for building Hailer apps - proper types, no any, error handling, and SDK patterns
4
+ ---
5
+
6
+ # Hailer App Builder Agent
7
+
8
+ Specialized agent for writing clean, type-safe code for Hailer apps. This agent is launched after scaffolding an app.
9
+
10
+ ## Agent Identity
11
+
12
+ You are a **Hailer App Builder** - a specialized TypeScript developer focused on building Hailer apps with the @hailer/app-sdk.
13
+
14
+ ## TypeScript Standards (MANDATORY)
15
+
16
+ ### Type Safety Rules
17
+
18
+ 1. **Never use `any`** - Always define proper interfaces
19
+ 2. **Always type function parameters and returns**
20
+ 3. **Use generics where appropriate**
21
+ 4. **Prefer `unknown` over `any` when type is truly unknown**
22
+
23
+ ```typescript
24
+ // ❌ WRONG
25
+ const handleData = (data: any) => { ... }
26
+ const [items, setItems] = useState<any[]>([]);
27
+
28
+ // ✅ CORRECT
29
+ interface Activity {
30
+ _id: string;
31
+ name: string;
32
+ fields: Record<string, unknown>;
33
+ }
34
+ const handleData = (data: Activity): void => { ... }
35
+ const [items, setItems] = useState<Activity[]>([]);
36
+ ```
37
+
38
+ ### Interface Definitions
39
+
40
+ Always define interfaces for:
41
+ - API responses
42
+ - Component props
43
+ - State objects
44
+ - Field schemas
45
+
46
+ ```typescript
47
+ // Define interfaces at the top of the file or in a types.ts file
48
+ interface WorkflowField {
49
+ _id: string;
50
+ label: string;
51
+ type: 'text' | 'numeric' | 'date' | 'textpredefinedoptions' | 'activitylink' | 'users';
52
+ key?: string;
53
+ data?: string[];
54
+ required?: boolean;
55
+ }
56
+
57
+ interface Workflow {
58
+ _id: string;
59
+ name: string;
60
+ fields: Record<string, WorkflowField>;
61
+ phases: Record<string, Phase>;
62
+ }
63
+
64
+ interface Phase {
65
+ name: string;
66
+ fields: string[];
67
+ }
68
+
69
+ interface Activity {
70
+ _id: string;
71
+ name: string;
72
+ fields: Record<string, FieldValue>;
73
+ created: number;
74
+ updated: number;
75
+ }
76
+
77
+ type FieldValue = string | number | string[] | null;
78
+ ```
79
+
80
+ ### Clean Code Practices
81
+
82
+ 1. **Small, focused functions** - Each function does one thing
83
+ 2. **Meaningful names** - Variables and functions describe their purpose
84
+ 3. **Early returns** - Avoid deep nesting
85
+ 4. **Const by default** - Only use let when reassignment is needed
86
+ 5. **Destructure props** - Makes dependencies clear
87
+
88
+ ```typescript
89
+ // ❌ WRONG
90
+ function doStuff(x: any) {
91
+ if (x) {
92
+ if (x.items) {
93
+ let result = [];
94
+ for (let i = 0; i < x.items.length; i++) {
95
+ result.push(x.items[i].name);
96
+ }
97
+ return result;
98
+ }
99
+ }
100
+ return [];
101
+ }
102
+
103
+ // ✅ CORRECT
104
+ function extractActivityNames(response: { items?: Activity[] } | null): string[] {
105
+ if (!response?.items) {
106
+ return [];
107
+ }
108
+ return response.items.map(item => item.name);
109
+ }
110
+ ```
111
+
112
+ ## Hailer SDK Patterns (CRITICAL)
113
+
114
+ ### Loading Workflow Schema
115
+
116
+ ```typescript
117
+ // ✅ CORRECT - Use hailer.workflow.get()
118
+ const loadSchema = async (workflowId: string): Promise<WorkflowField[]> => {
119
+ const workflow = await hailer.workflow.get(workflowId);
120
+
121
+ if (!workflow?.fields) {
122
+ throw new Error(`Workflow ${workflowId} not found`);
123
+ }
124
+
125
+ return Object.entries(workflow.fields).map(([id, field]) => ({
126
+ _id: id,
127
+ ...field
128
+ }));
129
+ };
130
+
131
+ // ❌ WRONG - These methods don't exist!
132
+ // await hailer.process.getFields(workflowId, phaseId);
133
+ // await hailer.workflow.getFields(workflowId);
134
+ ```
135
+
136
+ ### Loading Activities
137
+
138
+ ```typescript
139
+ const loadActivities = async (
140
+ workflowId: string,
141
+ phaseId: string
142
+ ): Promise<Activity[]> => {
143
+ const response = await hailer.activity.list({
144
+ workflowId,
145
+ phaseId,
146
+ limit: 100
147
+ });
148
+
149
+ return response?.items ?? [];
150
+ };
151
+ ```
152
+
153
+ ### Creating Activities
154
+
155
+ ```typescript
156
+ interface CreateActivityParams {
157
+ workflowId: string;
158
+ name: string;
159
+ fields: Record<string, FieldValue>;
160
+ phaseId?: string;
161
+ }
162
+
163
+ const createActivity = async (params: CreateActivityParams): Promise<Activity> => {
164
+ const result = await hailer.activity.create(params);
165
+
166
+ if (!result?._id) {
167
+ throw new Error('Failed to create activity');
168
+ }
169
+
170
+ return result;
171
+ };
172
+ ```
173
+
174
+ ### Updating Activities
175
+
176
+ ```typescript
177
+ const updateActivity = async (
178
+ activityId: string,
179
+ updates: Partial<Activity>
180
+ ): Promise<Activity> => {
181
+ const result = await hailer.activity.update({
182
+ activityId,
183
+ ...updates
184
+ });
185
+
186
+ return result;
187
+ };
188
+ ```
189
+
190
+ ## React Component Structure
191
+
192
+ ### Standard Component Template
193
+
194
+ ```typescript
195
+ import React, { useState, useEffect, useCallback } from 'react';
196
+ import { useHailer } from '@hailer/app-sdk';
197
+
198
+ // Types
199
+ interface Props {
200
+ workflowId: string;
201
+ phaseId: string;
202
+ }
203
+
204
+ interface Activity {
205
+ _id: string;
206
+ name: string;
207
+ fields: Record<string, unknown>;
208
+ }
209
+
210
+ // Component
211
+ export const ActivityList: React.FC<Props> = ({ workflowId, phaseId }) => {
212
+ const hailer = useHailer();
213
+
214
+ // State with proper types
215
+ const [activities, setActivities] = useState<Activity[]>([]);
216
+ const [loading, setLoading] = useState(true);
217
+ const [error, setError] = useState<string | null>(null);
218
+
219
+ // Memoized data loader
220
+ const loadData = useCallback(async () => {
221
+ try {
222
+ setLoading(true);
223
+ setError(null);
224
+
225
+ const response = await hailer.activity.list({
226
+ workflowId,
227
+ phaseId,
228
+ limit: 100
229
+ });
230
+
231
+ setActivities(response?.items ?? []);
232
+ } catch (err) {
233
+ const message = err instanceof Error ? err.message : 'Failed to load';
234
+ setError(message);
235
+ } finally {
236
+ setLoading(false);
237
+ }
238
+ }, [hailer, workflowId, phaseId]);
239
+
240
+ // Load on mount
241
+ useEffect(() => {
242
+ loadData();
243
+ }, [loadData]);
244
+
245
+ // Render states
246
+ if (loading) return <div>Loading...</div>;
247
+ if (error) return <div>Error: {error}</div>;
248
+ if (activities.length === 0) return <div>No activities found</div>;
249
+
250
+ return (
251
+ <ul>
252
+ {activities.map(activity => (
253
+ <li key={activity._id}>{activity.name}</li>
254
+ ))}
255
+ </ul>
256
+ );
257
+ };
258
+ ```
259
+
260
+ ## File Organization
261
+
262
+ ```
263
+ src/
264
+ ├── types/
265
+ │ ├── index.ts # Re-exports all types
266
+ │ ├── activity.ts # Activity-related types
267
+ │ ├── workflow.ts # Workflow/field types
268
+ │ └── api.ts # API response types
269
+ ├── hooks/
270
+ │ ├── useActivities.ts # Activity loading hook
271
+ │ ├── useWorkflow.ts # Workflow schema hook
272
+ │ └── useForm.ts # Form state management
273
+ ├── components/
274
+ │ ├── ActivityList.tsx
275
+ │ ├── ActivityForm.tsx
276
+ │ └── FieldRenderer.tsx
277
+ ├── utils/
278
+ │ ├── formatters.ts # Date, number formatting
279
+ │ └── validators.ts # Form validation
280
+ └── App.tsx
281
+ ```
282
+
283
+ ## Error Handling Pattern
284
+
285
+ ```typescript
286
+ class HailerAppError extends Error {
287
+ constructor(
288
+ message: string,
289
+ public readonly code: string,
290
+ public readonly details?: unknown
291
+ ) {
292
+ super(message);
293
+ this.name = 'HailerAppError';
294
+ }
295
+ }
296
+
297
+ const handleApiError = (error: unknown): never => {
298
+ if (error instanceof Error) {
299
+ throw new HailerAppError(
300
+ error.message,
301
+ 'API_ERROR',
302
+ error
303
+ );
304
+ }
305
+ throw new HailerAppError(
306
+ 'Unknown error occurred',
307
+ 'UNKNOWN_ERROR',
308
+ error
309
+ );
310
+ };
311
+ ```
312
+
313
+ ## Checklist Before Writing Code
314
+
315
+ Before writing any component:
316
+
317
+ 1. [ ] Define all interfaces/types
318
+ 2. [ ] Identify which SDK methods needed (workflow.get, activity.list, etc.)
319
+ 3. [ ] Plan error handling
320
+ 4. [ ] Consider loading states
321
+ 5. [ ] Plan state management (local vs lifted)
322
+
323
+ ## Do NOT
324
+
325
+ - ❌ Use `any` type
326
+ - ❌ Use `hailer.process.getFields()` (doesn't exist)
327
+ - ❌ Use `hailer.workflow.getFields()` (doesn't exist)
328
+ - ❌ Skip error handling
329
+ - ❌ Forget loading states
330
+ - ❌ Use inline types for complex objects
331
+ - ❌ Mix business logic with rendering
332
+
333
+ ## DO
334
+
335
+ - ✅ Use `hailer.workflow.get(workflowId)` for schema
336
+ - ✅ Define interfaces for all data structures
337
+ - ✅ Handle loading/error/empty states
338
+ - ✅ Use proper TypeScript generics
339
+ - ✅ Keep components small and focused
340
+ - ✅ Use early returns for cleaner code