@hailer/mcp 1.3.14 → 1.3.23

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 (57) hide show
  1. package/.claude/skills/create-and-publish-app/SKILL.md +44 -56
  2. package/.claude/skills/hailer-app-builder/SKILL.md +414 -970
  3. package/.claude/skills/hailer-app-primitives/SKILL.md +742 -0
  4. package/.claude/skills/hailer-apps-pictures/SKILL.md +191 -87
  5. package/.claude/skills/hailer-design-patterns/SKILL.md +317 -0
  6. package/.claude/skills/hailer-design-system/SKILL.md +202 -149
  7. package/.claude/skills/hailer-workflow-archetypes/SKILL.md +301 -0
  8. package/.claude/skills/insight-join-patterns/SKILL.md +313 -0
  9. package/.claude/skills/publish-hailer-app/SKILL.md +211 -0
  10. package/.claude/skills/sdk-activity-patterns/SKILL.md +257 -105
  11. package/.claude/skills/sdk-function-fields/SKILL.md +253 -492
  12. package/.claude/skills/sdk-insight-calculations/SKILL.md +364 -0
  13. package/.claude/skills/sdk-insight-queries/SKILL.md +192 -397
  14. package/.claude/skills/sdk-ws-config-skill/SKILL.md +265 -720
  15. package/.claude/skills/tool-response-verification/SKILL.md +143 -0
  16. package/CLAUDE.md +50 -19
  17. package/dist/bot/bot.d.ts.map +1 -1
  18. package/dist/bot/bot.js +26 -34
  19. package/dist/bot/bot.js.map +1 -1
  20. package/dist/bot/services/helper-prompt.d.ts +1 -1
  21. package/dist/bot/services/helper-prompt.d.ts.map +1 -1
  22. package/dist/bot/services/helper-prompt.js +62 -82
  23. package/dist/bot/services/helper-prompt.js.map +1 -1
  24. package/dist/bot/services/system-prompt.js +4 -4
  25. package/dist/config.d.ts +1 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +6 -0
  28. package/dist/config.js.map +1 -1
  29. package/dist/mcp/hailer-rpc.d.ts +1 -0
  30. package/dist/mcp/hailer-rpc.d.ts.map +1 -1
  31. package/dist/mcp/hailer-rpc.js +4 -0
  32. package/dist/mcp/hailer-rpc.js.map +1 -1
  33. package/dist/mcp/tools/activity.d.ts.map +1 -1
  34. package/dist/mcp/tools/activity.js +8 -2
  35. package/dist/mcp/tools/activity.js.map +1 -1
  36. package/dist/mcp/tools/app-core.d.ts.map +1 -1
  37. package/dist/mcp/tools/app-core.js +6 -3
  38. package/dist/mcp/tools/app-core.js.map +1 -1
  39. package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
  40. package/dist/mcp/tools/app-marketplace.js +5 -1
  41. package/dist/mcp/tools/app-marketplace.js.map +1 -1
  42. package/dist/mcp/webhook-handler.d.ts.map +1 -1
  43. package/dist/mcp/webhook-handler.js +27 -0
  44. package/dist/mcp/webhook-handler.js.map +1 -1
  45. package/dist/public-chat/graduate.d.ts.map +1 -1
  46. package/dist/public-chat/graduate.js +153 -53
  47. package/dist/public-chat/graduate.js.map +1 -1
  48. package/dist/public-chat/rate-limit.js +1 -1
  49. package/dist/public-chat/rate-limit.js.map +1 -1
  50. package/dist/public-chat/studio-prewarm.js +2 -2
  51. package/dist/public-chat/studio-prewarm.js.map +1 -1
  52. package/dist/public-chat/system-prompt.d.ts.map +1 -1
  53. package/dist/public-chat/system-prompt.js +41 -25
  54. package/dist/public-chat/system-prompt.js.map +1 -1
  55. package/package.json +1 -1
  56. package/.claude/skills/hailer-project-protocol/SKILL.md +0 -398
  57. package/.opencode/package-lock.json +0 -117
@@ -1,80 +1,112 @@
1
1
  ---
2
2
  name: hailer-apps-pictures
3
- description: Pattern for fetching and displaying images from Hailer in React apps
4
- version: 1.0.0
5
- triggers: When building Hailer apps that need to display images from activities or insights
3
+ description: Fetching and displaying images from Hailer in React apps — load when an app needs to show activity images, handle file uploads, build image carousels, or debug 403 image errors
4
+ version: 2.4.3
5
+ triggers:
6
+ - image
7
+ - picture
8
+ - photo
9
+ - file upload
10
+ - display image
11
+ - HailerImage
6
12
  ---
7
13
 
8
14
  # Hailer Apps Image Handling
9
15
 
10
16
  Pattern for fetching and displaying images from Hailer in React apps.
11
17
 
18
+ ## Permissions / Auth Requirements
19
+
20
+ **Standard uploads (via the Hailer UI file modifier or `uploadFile` without `isPublic: true`) are auth-gated.**
21
+
22
+ A viewer must have **workflow read membership** on the workflow that owns the activity holding the file. Without it, image requests return 403. This applies even inside Hailer apps.
23
+
24
+ | Upload path | Public? | Required to view |
25
+ |---|---|---|
26
+ | `uploadFile(file, name, { isPublic: true })` | Yes — via /public/ routes | Nothing |
27
+ | `uploadFile(file, name, { isPublic: false })` (or default) | No | Workflow read membership |
28
+ | Hailer UI file modifier (standard attachment) | No | Workflow read membership |
29
+
30
+ **`isPublic` must be set at upload time** — there is no API to flip a file from private to public after the fact.
31
+
32
+ **Practical design checklist:**
33
+ 1. Will all app users have workflow read on the source workflow? → standard upload works.
34
+ 2. Are some users members of the insight but NOT the source workflow? → grant them workflow read too.
35
+ 3. Is the image truly public-facing (embed in email, external site)? → upload with `{ isPublic: true }` and use the `/public/image/{size}/{id}` route.
36
+
37
+ ---
38
+
12
39
  ## Image URL Format
13
40
 
14
- Hailer serves images from this endpoint:
15
- ```
16
- https://api.hailer.com/image/hires/{imageId}
17
- ```
41
+ Two route families choose based on context:
42
+
43
+ | Route | When to use |
44
+ |-------|-------------|
45
+ | `/image/{size}/{id}` | Same-origin React app with active Hailer session (hlrkey cookie present) |
46
+ | `/public/image/{size}/{id}` | External embedding, email clients, cookieless cross-origin fetch, Node.js without a session — `isPublic: true` files only |
47
+
48
+ **The bare `/image/{size}/{id}` route returns 403 unauthenticated regardless of `isPublic`.** For any context where you cannot guarantee an active Hailer session cookie, use `/public/image/...`. (Live-verified 2026-06-10.)
18
49
 
19
50
  ### URL Patterns
20
51
 
21
- **Correct formats (use these):**
22
- - `https://api.hailer.com/image/hires/{imageId}` - High-resolution images, works without authentication
23
- - `https://api.hailer.com/image/thumbnail/{imageId}` - Thumbnail version for lists/grids
52
+ **Correct formats:**
53
+ - `https://api.hailer.com/image/hires/{imageId}` same-origin session; requires auth (hlrkey cookie)
54
+ - `https://api.hailer.com/image/thumb/{imageId}` — same-origin session thumbnail
55
+ - `https://api.hailer.com/public/image/hires/{imageId}` — external/cookieless; `isPublic: true` files only
56
+ - `https://api.hailer.com/public/image/thumb/{imageId}` — external/cookieless thumbnail
57
+
58
+ Valid size tokens: `square25, square50, square75, square100, square200, profile, smallface, bigface, thumb, gallerycover, fullhd, hires, lores, pdf, original`
24
59
 
25
60
  **Wrong formats (don't use):**
26
- - `https://app.hailer.com/api/files/{imageId}` - CORS issues in browsers
27
- - `https://api.hailer.com/files/{imageId}` - Authentication required, may fail in apps
28
- - `https://api.hailer.com/image/{imageId}` - Missing size parameter
61
+ - `https://app.hailer.com/api/files/{imageId}` CORS issues in browsers
62
+ - `https://api.hailer.com/files/{imageId}` authentication required, may fail in apps
63
+ - `https://api.hailer.com/image/{imageId}` missing size parameter
29
64
 
30
65
  ### Utility Function
31
66
 
32
67
  ```typescript
33
- function getImageUrl(imageId: string, size: 'hires' | 'thumbnail' = 'hires'): string {
34
- return `https://api.hailer.com/image/${size}/${imageId}`;
68
+ function getImageUrl(
69
+ imageId: string,
70
+ size: 'hires' | 'thumb' = 'hires',
71
+ options: { external?: boolean } = {}
72
+ ): string {
73
+ return options.external
74
+ ? `https://api.hailer.com/public/image/${size}/${imageId}`
75
+ : `https://api.hailer.com/image/${size}/${imageId}`;
35
76
  }
36
77
 
37
- // Usage
38
- const url = getImageUrl('abc123', 'hires'); // https://api.hailer.com/image/hires/abc123
39
- const thumbUrl = getImageUrl('abc123', 'thumbnail'); // https://api.hailer.com/image/thumbnail/abc123
78
+ // Same-origin Hailer app (session cookie present):
79
+ const url = getImageUrl('abc123', 'hires');
80
+ // External embed / email / cookieless (isPublic: true files only):
81
+ const externalUrl = getImageUrl('abc123', 'hires', { external: true });
82
+ // Note: 'thumbnail' is NOT a valid size token — use 'thumb'
40
83
  ```
41
84
 
42
85
  ### Extracting imageId from Activity Fields
43
86
 
44
- Image IDs typically come from file fields in activities. Extract them from the activity structure:
45
-
46
87
  ```typescript
47
- // From a file field in an activity
48
- const fileField = activity.fields.image_file; // e.g., { id: "file_xyz", name: "photo.jpg" }
49
- const imageId = fileField?.id;
88
+ // From a file-modifier field in an activity — value is a JSON-stringified array of file IDs
89
+ const ids: string[] = JSON.parse(activity.fields.image_file ?? '[]');
90
+ const imageId = ids[0];
50
91
 
51
92
  // From insight data with embedded URLs
52
93
  const imageUrl = item.mainProductImageUrl; // e.g., "https://api.hailer.com/image/hires/abc123"
53
- const imageId = imageUrl.split('/').pop(); // Extract from end of URL
94
+ const imageId = imageUrl.split('/').pop();
54
95
 
55
96
  // From JSON array of IDs
56
97
  const imageIds = JSON.parse(item.allPictures); // ["id1", "id2", "id3"]
57
98
  ```
58
99
 
59
- ## Data Structure
60
-
61
- Product/activity images typically come from insights with two fields:
62
-
63
- ```typescript
64
- interface WithImages {
65
- mainProductImageUrl: string; // Full URL to main image
66
- allPictures: string; // JSON string: '["id1", "id2", "id3"]'
67
- }
68
- ```
100
+ ---
69
101
 
70
102
  ## HailerImage Component
71
103
 
72
- Create a wrapper component for CORS handling and error states:
104
+ **iOS app version compatibility:** This component works as-is on iOS 2.2.4+ and all web. On older iOS app versions (pre-2.2.4), WKWebView blocks third-party auth cookies inside the `apps.hailer.com` iframe → `api.hailer.com` image requests fail.
73
105
 
74
106
  ```tsx
75
107
  // components/HailerImage.tsx
76
108
  import { Image, ImageProps, Icon } from '@chakra-ui/react';
77
- import { FaImage } from 'react-icons/fa6';
109
+ import { HailerImage as HailerImageIcon } from '../hailer/theme/icons/HailerImage'; // aliased — this component is also named HailerImage. No barrel index in the icons dir; import the specific file.
78
110
  import { useState, useEffect } from 'react';
79
111
  import useHailer from '../hailer/use-hailer';
80
112
 
@@ -89,23 +121,15 @@ export const HailerImage = ({
89
121
  alt = 'Image',
90
122
  ...imageProps
91
123
  }: HailerImageProps) => {
92
- const { inside, hailer } = useHailer(); // ← CORRECT: Destructure from useHailer()
124
+ const { inside, hailer } = useHailer();
93
125
  const [blobUrl, setBlobUrl] = useState<string | null>(null);
94
126
  const [isLoading, setIsLoading] = useState(true);
95
127
  const [isError, setIsError] = useState(false);
96
128
 
97
129
  useEffect(() => {
98
130
  const loadImage = async () => {
99
- if (!hailerImageUrl) {
100
- setIsError(true);
101
- setIsLoading(false);
102
- return;
103
- }
104
-
105
- if (!hailer?.ready) { // NOTE: verify .ready exists on SDK type — may need .activity or similar
106
- setIsLoading(true);
107
- return;
108
- }
131
+ if (!hailerImageUrl) { setIsError(true); setIsLoading(false); return; }
132
+ if (!inside) { setIsLoading(true); return; }
109
133
 
110
134
  try {
111
135
  const isHailerApiUrl = hailerImageUrl.includes('api.hailer.com');
@@ -113,7 +137,6 @@ export const HailerImage = ({
113
137
 
114
138
  if (isLocalDevelopment && isHailerApiUrl) {
115
139
  // CORS blocks api.hailer.com in localhost
116
- console.log('Local development: Hailer images blocked by CORS. Will work in production.');
117
140
  setIsError(true);
118
141
  setIsLoading(false);
119
142
  return;
@@ -122,7 +145,6 @@ export const HailerImage = ({
122
145
  setBlobUrl(hailerImageUrl);
123
146
  setIsError(false);
124
147
  } catch (error) {
125
- console.error('Error loading image from Hailer:', error);
126
148
  setIsError(true);
127
149
  } finally {
128
150
  setIsLoading(false);
@@ -132,15 +154,13 @@ export const HailerImage = ({
132
154
  loadImage();
133
155
 
134
156
  return () => {
135
- if (blobUrl) {
136
- URL.revokeObjectURL(blobUrl);
137
- }
157
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
138
158
  };
139
- }, [hailerImageUrl, hailer?.ready]);
159
+ }, [hailerImageUrl, inside]);
140
160
 
141
161
  if (isLoading || isError || !blobUrl) {
142
162
  return fallbackIcon ? (
143
- <Icon as={FaImage} color="gray.200" {...imageProps} />
163
+ <Icon as={HailerImageIcon} color="gray.200" {...imageProps} />
144
164
  ) : null;
145
165
  }
146
166
 
@@ -171,26 +191,22 @@ export const HailerImage = ({
171
191
 
172
192
  ## Usage: Image Carousel
173
193
 
174
- For multiple images stored as JSON array of IDs:
194
+ For multiple images stored as a JSON array of IDs:
175
195
 
176
196
  ```tsx
177
197
  const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
178
198
  const [currentIndex, setCurrentIndex] = useState(0);
179
199
 
180
- // Parse JSON string to array of IDs
181
200
  const imageIds = useMemo(() => {
182
201
  if (!allPictures) return [];
183
202
  try {
184
203
  const parsed = JSON.parse(allPictures);
185
204
  return Array.isArray(parsed) ? parsed : [];
186
- } catch {
187
- return [];
188
- }
205
+ } catch { return []; }
189
206
  }, [allPictures]);
190
207
 
191
208
  const hasMultiple = imageIds.length > 1;
192
209
 
193
- // Construct URL from ID
194
210
  const getCurrentUrl = () => {
195
211
  if (imageIds.length === 0) return mainImageUrl || '';
196
212
  const id = imageIds[currentIndex];
@@ -206,25 +222,21 @@ const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
206
222
 
207
223
  {hasMultiple && (
208
224
  <>
225
+ {/* Use the Hailer design-system chevron — never external icon libs.
226
+ Apply a rotate() transform if it doesn't already point left/right. */}
209
227
  <IconButton
210
- icon={<FaChevronLeft />}
228
+ aria-label="Previous image"
229
+ icon={<HailerChevron />}
211
230
  onClick={goPrev}
212
- position="absolute"
213
- left={2}
214
- top="50%"
215
- transform="translateY(-50%)"
231
+ position="absolute" left={2} top="50%" transform="translateY(-50%)"
216
232
  />
217
233
  <IconButton
218
- icon={<FaChevronRight />}
234
+ aria-label="Next image"
235
+ icon={<HailerChevron />}
219
236
  onClick={goNext}
220
- position="absolute"
221
- right={2}
222
- top="50%"
223
- transform="translateY(-50%)"
237
+ position="absolute" right={2} top="50%" transform="translateY(-50%)"
224
238
  />
225
- <Text textAlign="center">
226
- {currentIndex + 1} / {imageIds.length}
227
- </Text>
239
+ <Text textAlign="center">{currentIndex + 1} / {imageIds.length}</Text>
228
240
  </>
229
241
  )}
230
242
  </Box>
@@ -232,38 +244,130 @@ const ProductImageCarousel = ({ allPictures, mainImageUrl }: Props) => {
232
244
  };
233
245
  ```
234
246
 
235
- ## Fetching Image Data from Insights
247
+ ---
248
+
249
+ ## File Upload Pattern
250
+
251
+ For app-sdk 2.4.x+, use `hailer.file.upload()` — `hailer.ui.files.uploadFile()` is marked `@deprecated` in 2.4.x. Both have the same signature and return a `fileId` string.
236
252
 
237
253
  ```typescript
238
- // In App.tsx or data fetching hook
239
- const fetchProducts = async () => {
240
- const data = await hailer.public.insight.dataAsObject(PRODUCT_INSIGHT_KEY);
241
-
242
- return data.map(item => ({
243
- // ... other fields
244
- mainProductImageUrl: item.mainProductImageUrl || '',
245
- allPictures: item.allPictures || '[]',
246
- }));
254
+ // Preferred for app-sdk 2.4.x+
255
+ const fileId = await hailer.file.upload(file, fileName, { isPublic: false });
256
+ const publicFileId = await hailer.file.upload(file, fileName, { isPublic: true });
257
+
258
+ // Legacy (still works but deprecated in 2.4.x)
259
+ const fileId = await hailer.ui.files.uploadFile(file, fileName, { isPublic: false });
260
+
261
+ // Pass fileIds when creating an activity
262
+ await hailer.activity.create('workflowId', [
263
+ { name: 'Activity', fields: { /* ... */ } }
264
+ ], { fileIds: [fileId] });
265
+ ```
266
+
267
+ **`isPublic: true` must be set at upload time.** There is no API to promote a file from private to public after creation.
268
+
269
+ **`fileIds` only bind at `activity.create`.** There is no `attachFiles`/`addFiles` method on app-sdk. Defer activity creation until files are ready.
270
+
271
+ ### Canonical upload-during-form-fill pattern
272
+
273
+ ```tsx
274
+ const uploadPromiseRef = useRef<Promise<string[]> | null>(null);
275
+
276
+ // When user selects files — start upload immediately
277
+ const handleFilesSelected = (files: File[]) => {
278
+ uploadPromiseRef.current = Promise.all(
279
+ files.map(f => hailer.file.upload(f, f.name, {}))
280
+ );
247
281
  };
282
+
283
+ // On Save — await the in-flight promise, then create atomically
284
+ const handleSave = async () => {
285
+ const fileIds = uploadPromiseRef.current ? await uploadPromiseRef.current : [];
286
+ await hailer.activity.create(workflowId, [{ name, fields }], { fileIds });
287
+ };
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Server-to-Server Image Fetching
293
+
294
+ For **public files** (`isPublic: true`), use `/public/image/{size}/{id}` for cookieless or external fetches. The bare `/image/{size}/{id}` route requires the `hlrkey` cookie — it returns 403 unauthenticated regardless of `isPublic` (live-verified 2026-06-10).
295
+
296
+ ```
297
+ # Cookieless/external (isPublic files only):
298
+ https://api.hailer.com/public/image/{size}/{imageId}
299
+
300
+ # Same-origin / hlrkey-authenticated:
301
+ https://api.hailer.com/image/{size}/{imageId}
248
302
  ```
249
303
 
304
+ Diagnostic: 404 on `/public/` = file not isPublic; 403 on bare route = missing auth.
305
+
306
+ ---
307
+
250
308
  ## Key Points
251
309
 
252
310
  | Aspect | Details |
253
311
  |--------|---------|
254
- | **URL Format** | `https://api.hailer.com/image/hires/{imageId}` |
255
- | **CORS** | Blocked in localhost, works in production |
256
- | **Fallback** | Use `FaImage` icon from react-icons/fa6 |
312
+ | **URL Format** | Same-origin session: `/image/{size}/{id}`; external/cookieless: `/public/image/{size}/{id}` |
313
+ | **Auth — standard files** | Requires workflow read membership; bare `/image/` route only |
314
+ | **Auth — public files** | `isPublic: true` at upload time; use `/public/image/` for cookieless access |
315
+ | **CORS** | Blocked in localhost browser, works in production and from Node.js |
316
+ | **Fallback** | Use the design-system `HailerImage` icon (import aliased to `HailerImageIcon`) |
257
317
  | **Performance** | Use `loading="lazy"` for catalog images |
258
318
  | **Multiple Images** | Store as JSON array of IDs, construct URLs on demand |
259
319
  | **Memory** | Revoke blob URLs on component cleanup |
260
320
 
321
+ ---
322
+
323
+ ## Diagnostic Overlay Pattern (iOS / Debug Builds)
324
+
325
+ Use when diagnosing image load failures on iOS — when console access isn't available.
326
+
327
+ ```typescript
328
+ // At module top — NOT inside the component
329
+ const imageLoadStats = {
330
+ directLoadSuccess: 0,
331
+ S0_filget: 0,
332
+ S1_rpc: 0,
333
+ error: 0,
334
+ };
335
+ ```
336
+
337
+ ```tsx
338
+ // components/DiagnosticOverlay.tsx
339
+ import { useEffect, useState } from 'react';
340
+ import { imageLoadStats } from '../components/YourImageComponent';
341
+
342
+ export const DiagnosticOverlay = () => {
343
+ const [snap, setSnap] = useState({ ...imageLoadStats });
344
+
345
+ useEffect(() => {
346
+ const id = setInterval(() => setSnap({ ...imageLoadStats }), 500);
347
+ return () => clearInterval(id);
348
+ }, []);
349
+
350
+ return (
351
+ <div style={{
352
+ position: 'fixed', bottom: 0, left: 0, right: 0,
353
+ background: 'rgba(0,0,0,0.75)', color: '#fff',
354
+ fontSize: 11, padding: '4px 8px', zIndex: 9999,
355
+ }}>
356
+ direct:{snap.directLoadSuccess} S0:{snap.S0_filget} S1:{snap.S1_rpc} err:{snap.error}
357
+ </div>
358
+ );
359
+ };
360
+ ```
361
+
362
+ Mount once in `App.tsx`. Bump the manifest version for each diagnostic build; strip the overlay after the fix is verified.
363
+
364
+ ---
365
+
261
366
  ## Dependencies
262
367
 
263
368
  ```json
264
369
  {
265
370
  "@chakra-ui/react": "^2.x",
266
- "react-icons": "^5.x",
267
371
  "@hailer/app-sdk": "^2.x"
268
372
  }
269
373
  ```