@hailer/mcp 1.1.11 → 1.1.13

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 (252) hide show
  1. package/dist/app.js +18 -5
  2. package/dist/bot/bot-config.d.ts +12 -1
  3. package/dist/bot/bot-config.js +98 -14
  4. package/dist/bot/bot-manager.d.ts +13 -3
  5. package/dist/bot/bot-manager.js +80 -25
  6. package/dist/bot/bot.d.ts +46 -0
  7. package/dist/bot/bot.js +542 -166
  8. package/dist/bot/services/message-classifier.js +17 -0
  9. package/dist/bot/services/permission-guard.d.ts +52 -0
  10. package/dist/bot/services/permission-guard.js +149 -0
  11. package/dist/bot/services/types.d.ts +5 -0
  12. package/dist/bot/services/typing-indicator.d.ts +6 -1
  13. package/dist/bot/services/typing-indicator.js +19 -3
  14. package/dist/config.d.ts +6 -1
  15. package/dist/config.js +43 -0
  16. package/dist/core.js +3 -6
  17. package/dist/mcp/UserContextCache.d.ts +5 -0
  18. package/dist/mcp/UserContextCache.js +51 -19
  19. package/dist/mcp/hailer-clients.d.ts +19 -1
  20. package/dist/mcp/hailer-clients.js +157 -20
  21. package/dist/mcp/session-store.d.ts +68 -0
  22. package/dist/mcp/session-store.js +169 -0
  23. package/dist/mcp/signal-handler.js +12 -12
  24. package/dist/mcp/tool-registry.d.ts +17 -4
  25. package/dist/mcp/tool-registry.js +37 -7
  26. package/dist/mcp/tools/activity.js +99 -7
  27. package/dist/mcp/tools/app-scaffold.js +304 -336
  28. package/dist/mcp/tools/company.d.ts +9 -0
  29. package/dist/mcp/tools/company.js +88 -0
  30. package/dist/mcp/tools/discussion.js +68 -0
  31. package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
  32. package/dist/mcp/tools/workflow-permissions.js +204 -0
  33. package/dist/mcp/tools/workflow.js +57 -18
  34. package/dist/mcp/utils/index.d.ts +2 -0
  35. package/dist/mcp/utils/index.js +12 -1
  36. package/dist/mcp/utils/role-utils.d.ts +74 -0
  37. package/dist/mcp/utils/role-utils.js +151 -0
  38. package/dist/mcp/utils/types.d.ts +43 -1
  39. package/dist/mcp/utils/types.js +14 -0
  40. package/dist/mcp/webhook-handler.d.ts +6 -0
  41. package/dist/mcp/webhook-handler.js +11 -0
  42. package/dist/mcp-server.d.ts +23 -2
  43. package/dist/mcp-server.js +639 -111
  44. package/dist/plugins/vipunen/client.d.ts +150 -0
  45. package/dist/plugins/vipunen/client.js +535 -0
  46. package/dist/plugins/vipunen/config/schema-config.json +19 -0
  47. package/dist/plugins/vipunen/config/schema-doc.json +22 -0
  48. package/dist/plugins/vipunen/index.d.ts +41 -0
  49. package/dist/plugins/vipunen/index.js +88 -0
  50. package/dist/plugins/vipunen/tools.d.ts +26 -0
  51. package/dist/plugins/vipunen/tools.js +501 -0
  52. package/package.json +2 -1
  53. package/.claude/.context-watchdog.json +0 -1
  54. package/.claude/.session-checked +0 -1
  55. package/.claude/CLAUDE.md +0 -370
  56. package/.claude/agents/agent-ada-skill-builder.md +0 -94
  57. package/.claude/agents/agent-alejandro-function-fields.md +0 -342
  58. package/.claude/agents/agent-bjorn-config-audit.md +0 -103
  59. package/.claude/agents/agent-builder-agent-creator.md +0 -130
  60. package/.claude/agents/agent-code-simplifier.md +0 -53
  61. package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
  62. package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
  63. package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
  64. package/.claude/agents/agent-helga-workflow-config.md +0 -204
  65. package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
  66. package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
  67. package/.claude/agents/agent-ivan-monolith.md +0 -154
  68. package/.claude/agents/agent-kenji-data-reader.md +0 -86
  69. package/.claude/agents/agent-lars-code-inspector.md +0 -102
  70. package/.claude/agents/agent-marco-mockup-builder.md +0 -110
  71. package/.claude/agents/agent-marcus-api-documenter.md +0 -323
  72. package/.claude/agents/agent-marketplace-publisher.md +0 -280
  73. package/.claude/agents/agent-marketplace-reviewer.md +0 -309
  74. package/.claude/agents/agent-permissions-handler.md +0 -208
  75. package/.claude/agents/agent-simple-writer.md +0 -48
  76. package/.claude/agents/agent-svetlana-code-review.md +0 -171
  77. package/.claude/agents/agent-tanya-test-runner.md +0 -333
  78. package/.claude/agents/agent-ui-designer.md +0 -100
  79. package/.claude/agents/agent-viktor-sql-insights.md +0 -212
  80. package/.claude/agents/agent-web-search.md +0 -55
  81. package/.claude/agents/agent-yevgeni-discussions.md +0 -45
  82. package/.claude/agents/agent-zara-zapier.md +0 -159
  83. package/.claude/commands/app-squad.md +0 -135
  84. package/.claude/commands/audit-squad.md +0 -158
  85. package/.claude/commands/autoplan.md +0 -563
  86. package/.claude/commands/cleanup-squad.md +0 -98
  87. package/.claude/commands/config-squad.md +0 -106
  88. package/.claude/commands/crud-squad.md +0 -87
  89. package/.claude/commands/data-squad.md +0 -97
  90. package/.claude/commands/debug-squad.md +0 -303
  91. package/.claude/commands/doc-squad.md +0 -65
  92. package/.claude/commands/handoff.md +0 -137
  93. package/.claude/commands/health.md +0 -49
  94. package/.claude/commands/help.md +0 -29
  95. package/.claude/commands/help:agents.md +0 -151
  96. package/.claude/commands/help:commands.md +0 -78
  97. package/.claude/commands/help:faq.md +0 -79
  98. package/.claude/commands/help:plugins.md +0 -50
  99. package/.claude/commands/help:skills.md +0 -93
  100. package/.claude/commands/help:tools.md +0 -75
  101. package/.claude/commands/hotfix-squad.md +0 -112
  102. package/.claude/commands/integration-squad.md +0 -82
  103. package/.claude/commands/janitor-squad.md +0 -167
  104. package/.claude/commands/learn-auto.md +0 -120
  105. package/.claude/commands/learn.md +0 -120
  106. package/.claude/commands/mcp-list.md +0 -27
  107. package/.claude/commands/onboard-squad.md +0 -140
  108. package/.claude/commands/plan-workspace.md +0 -732
  109. package/.claude/commands/prd.md +0 -130
  110. package/.claude/commands/project-status.md +0 -82
  111. package/.claude/commands/publish.md +0 -138
  112. package/.claude/commands/recap.md +0 -69
  113. package/.claude/commands/restore.md +0 -64
  114. package/.claude/commands/review-squad.md +0 -152
  115. package/.claude/commands/save.md +0 -24
  116. package/.claude/commands/stats.md +0 -19
  117. package/.claude/commands/swarm.md +0 -210
  118. package/.claude/commands/tool-builder.md +0 -39
  119. package/.claude/commands/ws-pull.md +0 -44
  120. package/.claude/hooks/_shared-memory.cjs +0 -305
  121. package/.claude/hooks/_utils.cjs +0 -108
  122. package/.claude/hooks/agent-failure-detector.cjs +0 -383
  123. package/.claude/hooks/agent-usage-logger.cjs +0 -204
  124. package/.claude/hooks/app-edit-guard.cjs +0 -494
  125. package/.claude/hooks/auto-learn.cjs +0 -304
  126. package/.claude/hooks/bash-guard.cjs +0 -272
  127. package/.claude/hooks/builder-mode-manager.cjs +0 -354
  128. package/.claude/hooks/bulk-activity-guard.cjs +0 -271
  129. package/.claude/hooks/context-watchdog.cjs +0 -230
  130. package/.claude/hooks/delegation-reminder.cjs +0 -465
  131. package/.claude/hooks/design-system-lint.cjs +0 -271
  132. package/.claude/hooks/post-scaffold-hook.cjs +0 -181
  133. package/.claude/hooks/prompt-guard.cjs +0 -354
  134. package/.claude/hooks/publish-template-guard.cjs +0 -147
  135. package/.claude/hooks/session-start.cjs +0 -35
  136. package/.claude/hooks/shared-memory-writer.cjs +0 -147
  137. package/.claude/hooks/skill-injector.cjs +0 -140
  138. package/.claude/hooks/skill-usage-logger.cjs +0 -258
  139. package/.claude/hooks/src-edit-guard.cjs +0 -240
  140. package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
  141. package/.claude/settings.json +0 -257
  142. package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
  143. package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
  144. package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
  145. package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
  146. package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
  147. package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
  148. package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
  149. package/.claude/skills/agent-structure/SKILL.md +0 -98
  150. package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
  151. package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
  152. package/.claude/skills/delegation-routing/SKILL.md +0 -202
  153. package/.claude/skills/frontend-design/SKILL.md +0 -254
  154. package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
  155. package/.claude/skills/hailer-api-client/SKILL.md +0 -518
  156. package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
  157. package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
  158. package/.claude/skills/hailer-design-system/SKILL.md +0 -235
  159. package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
  160. package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
  161. package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
  162. package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
  163. package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
  164. package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
  165. package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
  166. package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
  167. package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
  168. package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
  169. package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
  170. package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
  171. package/.claude/skills/integration-patterns/SKILL.md +0 -421
  172. package/.claude/skills/json-only-output/SKILL.md +0 -72
  173. package/.claude/skills/lsp-setup/SKILL.md +0 -160
  174. package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
  175. package/.claude/skills/optional-parameters/SKILL.md +0 -72
  176. package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
  177. package/.claude/skills/testing-patterns/SKILL.md +0 -630
  178. package/.claude/skills/tool-builder/SKILL.md +0 -250
  179. package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
  180. package/.claude/skills/tool-response-verification/SKILL.md +0 -92
  181. package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
  182. package/.hailer-mcp-port +0 -1
  183. package/.mcp.json +0 -13
  184. package/.opencode/agent/agent-ada-skill-builder.md +0 -35
  185. package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
  186. package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
  187. package/.opencode/agent/agent-builder-agent-creator.md +0 -39
  188. package/.opencode/agent/agent-code-simplifier.md +0 -31
  189. package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
  190. package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
  191. package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
  192. package/.opencode/agent/agent-helga-workflow-config.md +0 -204
  193. package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
  194. package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
  195. package/.opencode/agent/agent-ivan-monolith.md +0 -46
  196. package/.opencode/agent/agent-kenji-data-reader.md +0 -53
  197. package/.opencode/agent/agent-lars-code-inspector.md +0 -28
  198. package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
  199. package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
  200. package/.opencode/agent/agent-marketplace-publisher.md +0 -44
  201. package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
  202. package/.opencode/agent/agent-permissions-handler.md +0 -50
  203. package/.opencode/agent/agent-simple-writer.md +0 -45
  204. package/.opencode/agent/agent-svetlana-code-review.md +0 -39
  205. package/.opencode/agent/agent-tanya-test-runner.md +0 -57
  206. package/.opencode/agent/agent-ui-designer.md +0 -56
  207. package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
  208. package/.opencode/agent/agent-web-search.md +0 -42
  209. package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
  210. package/.opencode/agent/agent-zara-zapier.md +0 -53
  211. package/.opencode/commands/app-squad.md +0 -135
  212. package/.opencode/commands/audit-squad.md +0 -158
  213. package/.opencode/commands/autoplan.md +0 -563
  214. package/.opencode/commands/cleanup-squad.md +0 -98
  215. package/.opencode/commands/config-squad.md +0 -106
  216. package/.opencode/commands/crud-squad.md +0 -87
  217. package/.opencode/commands/data-squad.md +0 -97
  218. package/.opencode/commands/debug-squad.md +0 -303
  219. package/.opencode/commands/doc-squad.md +0 -65
  220. package/.opencode/commands/handoff.md +0 -137
  221. package/.opencode/commands/health.md +0 -49
  222. package/.opencode/commands/help-agents.md +0 -151
  223. package/.opencode/commands/help-commands.md +0 -32
  224. package/.opencode/commands/help-faq.md +0 -29
  225. package/.opencode/commands/help-plugins.md +0 -28
  226. package/.opencode/commands/help-skills.md +0 -7
  227. package/.opencode/commands/help-tools.md +0 -40
  228. package/.opencode/commands/help.md +0 -28
  229. package/.opencode/commands/hotfix-squad.md +0 -112
  230. package/.opencode/commands/integration-squad.md +0 -82
  231. package/.opencode/commands/janitor-squad.md +0 -167
  232. package/.opencode/commands/learn-auto.md +0 -120
  233. package/.opencode/commands/learn.md +0 -120
  234. package/.opencode/commands/mcp-list.md +0 -27
  235. package/.opencode/commands/onboard-squad.md +0 -140
  236. package/.opencode/commands/plan-workspace.md +0 -732
  237. package/.opencode/commands/prd.md +0 -131
  238. package/.opencode/commands/project-status.md +0 -82
  239. package/.opencode/commands/publish.md +0 -138
  240. package/.opencode/commands/recap.md +0 -69
  241. package/.opencode/commands/restore.md +0 -64
  242. package/.opencode/commands/review-squad.md +0 -152
  243. package/.opencode/commands/save.md +0 -24
  244. package/.opencode/commands/stats.md +0 -19
  245. package/.opencode/commands/swarm.md +0 -210
  246. package/.opencode/commands/tool-builder.md +0 -39
  247. package/.opencode/commands/ws-pull.md +0 -44
  248. package/.opencode/opencode.json +0 -21
  249. package/inbox/failures.log +0 -1
  250. package/inbox/usage.jsonl +0 -4
  251. package/scripts/postinstall.cjs +0 -64
  252. package/scripts/test-hal-tools.ts +0 -154
@@ -1,1434 +0,0 @@
1
- ---
2
- name: hailer-app-builder
3
- description: Patterns for building Hailer apps with @hailer/app-sdk
4
- version: 1.3.1
5
- triggers:
6
- - build app
7
- - hailer app
8
- - app sdk
9
- ---
10
-
11
- # Hailer App Builder Skill
12
-
13
- Patterns and templates for building Hailer apps with @hailer/app-sdk.
14
-
15
- <critical-rules>
16
- ## CRITICAL: Scaffolding and Data Sources
17
-
18
- **ALWAYS use scaffold_hailer_app MCP tool** to create new apps. Never manually create the project structure.
19
-
20
- ### scaffold_hailer_app - One-Shot Full Setup
21
-
22
- This tool does EVERYTHING in one call:
23
- - Scaffolds project from template (Vite + React + TypeScript)
24
- - Runs `npm install`
25
- - Configures CORS in `vite.config.ts`
26
- - **Creates dev app entry in Hailer** (with auto-generated icon)
27
- - Shares app with entire workspace
28
- - Adds appId to `manifest.json`
29
- - Starts dev server on port 3000
30
-
31
- **You're customizing a working starter app**, not building from scratch.
32
-
33
- ### create_app - Entry Only (No Local Files)
34
-
35
- Use `mcp__hailer__create_app` when you:
36
- - Need a production app entry pointing to a deployed URL
37
- - Want to register an external/existing app in Hailer
38
- - Already have app code and just need the Hailer entry
39
-
40
- ```
41
- mcp__hailer__create_app({
42
- name: "Production App",
43
- url: "https://app.example.com"
44
- })
45
- ```
46
-
47
- **scaffold = full development setup**
48
- **create_app = just the Hailer entry/frame**
49
-
50
- **For project data structure (workflows, fields, phases):**
51
- - READ workspace/ TypeScript files directly (fields.ts, phases.ts, enums.ts)
52
- - Do NOT use MCP tools for data structure queries
53
- - The SDK pull provides all needed type information locally
54
-
55
- ```typescript
56
- // For app constants, read from local workspace files:
57
- // - workspace/enums.ts (IDs)
58
- // - workspace/[Workflow]_[id]/fields.ts (field definitions)
59
- // - workspace/[Workflow]_[id]/phases.ts (phase definitions)
60
- ```
61
- </critical-rules>
62
-
63
- <local-dev-flow>
64
- ## Development Flow
65
-
66
- **Default: Local development.** `scaffold_hailer_app` handles everything automatically:
67
- 1. Creates local project files
68
- 2. Creates a dev app entry in Hailer at `http://localhost:3000`
69
- 3. Shares the app with the workspace
70
- 4. Starts the dev server
71
-
72
- After scaffolding, run `npm run dev` and test inside Hailer iframe.
73
-
74
- **Publishing: Only when user explicitly asks.** Load the `publish-hailer-app` skill, which covers manifest validation, file upload via `publish_hailer_app`, and URL switch from localhost to production via `update_app`.
75
-
76
- ### Manual Local Dev App (Rare Cases)
77
-
78
- Only needed if:
79
- - You have existing code without a dev app entry
80
- - The scaffold's dev app was deleted
81
-
82
- ```
83
- mcp__hailer__create_app({
84
- name: "Local Dev",
85
- url: "http://localhost:3000",
86
- description: "Local development testing"
87
- })
88
- ```
89
-
90
- Or manually in Hailer UI: Apps → Create App → URL: http://localhost:3000
91
- </local-dev-flow>
92
-
93
- <sdk-setup>
94
- ## Hook Import (CRITICAL)
95
-
96
- ```typescript
97
- // CORRECT - local default import
98
- import useHailer from './hailer/use-hailer';
99
-
100
- // WRONG - will fail build
101
- import { useHailer } from '@hailer/app-sdk';
102
- ```
103
-
104
- ## Hook Usage
105
-
106
- ```typescript
107
- function App() {
108
- const { inside, hailer } = useHailer();
109
-
110
- // CORRECT dependency array
111
- useEffect(() => {
112
- // fetch data
113
- }, [inside]); // Use [inside] NOT [hailer]
114
-
115
- // Early return AFTER hooks
116
- if (!inside) return <Text>Open this app inside Hailer</Text>;
117
-
118
- return <Box>...</Box>;
119
- }
120
- ```
121
- </sdk-setup>
122
-
123
- <usehailer-fix>
124
- ## CRITICAL: Replace Scaffold's useHailer Hook
125
-
126
- **The scaffold generates a buggy useHailer hook.** After scaffolding, ALWAYS replace `src/hailer/use-hailer.ts` with this shared-state implementation:
127
-
128
- ### Why the Scaffold's Hook is Broken
129
-
130
- The scaffold creates a hook using per-component `useState`:
131
-
132
- ```typescript
133
- // ❌ BUGGY - each component gets its own state
134
- function useHailer() {
135
- const [inside, setInside] = useState(false); // Each component gets separate copy!
136
-
137
- useEffect(() => {
138
- hailer.init({ config: () => setInside(true) }); // Only updates THIS component
139
- }, []);
140
-
141
- return { inside, hailer };
142
- }
143
- ```
144
-
145
- **Result:** App.tsx sees `inside: true`, but child pages (Dashboard, Settings) still see `inside: false`.
146
-
147
- ### The Fix: Shared State with useSyncExternalStore
148
-
149
- Replace `src/hailer/use-hailer.ts` with:
150
-
151
- ```typescript
152
- import { useSyncExternalStore } from 'react';
153
- import HailerApi from '@hailer/app-sdk';
154
-
155
- // Types
156
- interface HailerState {
157
- inside: boolean;
158
- hailer: ReturnType<typeof HailerApi> | null;
159
- config: Record<string, unknown> | null;
160
- }
161
-
162
- declare global {
163
- interface Window {
164
- __hailerStore?: {
165
- state: HailerState;
166
- listeners: Set<() => void>;
167
- subscribe: (listener: () => void) => () => void;
168
- getSnapshot: () => HailerState;
169
- setState: (newState: Partial<HailerState>) => void;
170
- };
171
- }
172
- }
173
-
174
- // Initialize store once on window
175
- function getStore() {
176
- if (!window.__hailerStore) {
177
- window.__hailerStore = {
178
- state: { inside: false, hailer: null, config: null },
179
- listeners: new Set(),
180
- subscribe(listener) {
181
- this.listeners.add(listener);
182
- return () => this.listeners.delete(listener);
183
- },
184
- getSnapshot() {
185
- return this.state;
186
- },
187
- setState(newState) {
188
- this.state = { ...this.state, ...newState };
189
- this.listeners.forEach((l) => l());
190
- },
191
- };
192
-
193
- // Initialize SDK once
194
- const api = HailerApi({
195
- config: (inside, cfg) => { // SDK passes (inside: boolean, config: object)
196
- window.__hailerStore!.setState({
197
- inside: inside,
198
- config: cfg?.fields ?? null,
199
- });
200
- },
201
- error: (err) => {
202
- console.error('Hailer SDK error:', err);
203
- },
204
- });
205
- window.__hailerStore.setState({ hailer: api });
206
- }
207
- return window.__hailerStore;
208
- }
209
-
210
- export default function useHailer() {
211
- const store = getStore();
212
- const state = useSyncExternalStore(
213
- store.subscribe.bind(store),
214
- store.getSnapshot.bind(store)
215
- );
216
-
217
- return {
218
- inside: state.inside,
219
- hailer: state.hailer!,
220
- config: state.config,
221
- };
222
- }
223
- ```
224
-
225
- ### Why This Works
226
-
227
- 1. **Single store on `window`** - All components share one source of truth
228
- 2. **SDK initialized once** - No duplicate callbacks
229
- 3. **useSyncExternalStore** - React 18's official pattern for external state
230
- 4. **All components update together** - When `inside` changes, every subscriber re-renders
231
-
232
- ### Giuseppe Rule
233
-
234
- After `scaffold_hailer_app`, ALWAYS replace `src/hailer/use-hailer.ts` with the shared-state version above.
235
- </usehailer-fix>
236
-
237
- <sdk-api>
238
- ## Activity API
239
-
240
- ```typescript
241
- // List activities from workflow phase
242
- const activities = await hailer.activity.list(workflowId, phaseId, options?);
243
-
244
- // Get single activity
245
- const activity = await hailer.activity.get(activityId);
246
-
247
- // Create activities
248
- const created = await hailer.activity.create(workflowId, activities[], options?);
249
-
250
- // Update activities (returns count)
251
- const count = await hailer.activity.update(activities[], options);
252
-
253
- // Remove activities (returns count)
254
- const count = await hailer.activity.remove(activityIds[]);
255
- ```
256
-
257
- ### ActivityListOptions
258
-
259
- ```typescript
260
- interface ActivityListOptions {
261
- sortBy?: 'name' | 'created' | 'updated' | 'following' | 'owner' | 'team' | 'completedOn' | 'priority';
262
- sortOrder?: 'asc' | 'desc';
263
- limit?: number;
264
- skip?: number;
265
- includeUsers?: boolean;
266
- includeTeams?: boolean;
267
- includeHistory?: boolean;
268
- filters?: any;
269
- }
270
- ```
271
-
272
- ### ActivityCreateOptions
273
-
274
- ```typescript
275
- interface ActivityCreateOptions {
276
- teamId?: string; // Assign to team
277
- fileIds?: string[]; // Attach files (use ui.files.uploadFile first)
278
- followerIds?: string[]; // Add followers
279
- location?: {
280
- label?: string;
281
- type: 'area' | 'point' | 'polyline';
282
- data: [{ lat: number; lng: number }];
283
- };
284
- discussionId?: string; // Link to discussion
285
- phaseId?: string; // Initial phase (defaults to first)
286
- returnDocument?: boolean; // Return full activity after create
287
- ignoreRequired?: boolean; // Skip required field validation
288
- }
289
- ```
290
-
291
- ### Activity Interface
292
-
293
- ```typescript
294
- interface Activity {
295
- _id: string;
296
- name: string;
297
- process: string;
298
- currentPhase: string;
299
- fields?: Record<string, { value: unknown }>;
300
- files?: string[];
301
- followers?: string[];
302
- created?: number;
303
- updated?: number;
304
- updatedBy?: string;
305
- priority?: number;
306
- location?: {
307
- type: 'point' | 'area' | 'polyline';
308
- label: string | null;
309
- data: Array<{ lat: number; lng: number }>;
310
- };
311
- }
312
- ```
313
-
314
- ### Phase Transitions (Moving Activities Between Phases)
315
-
316
- **CRITICAL:** The SDK does NOT have a `hailer.activity.move()` method. To move activities between phases, use `hailer.activity.update()` with the `phaseId` parameter.
317
-
318
- ```typescript
319
- // CORRECT - Use activity.update() with phaseId
320
- await hailer.activity.update([
321
- {
322
- _id: activityId,
323
- phaseId: newPhaseId, // Move to different phase
324
- },
325
- ], {});
326
-
327
- // ❌ WRONG - activity.move() DOES NOT EXIST
328
- // await hailer.activity.move(activityId, newPhaseId); // This method doesn't exist in the SDK!
329
- ```
330
-
331
- **Example: Move multiple activities to new phase**
332
- ```typescript
333
- const activityIds = ['id1', 'id2', 'id3'];
334
- const targetPhaseId = 'phaseId789';
335
-
336
- await hailer.activity.update(
337
- activityIds.map(id => ({
338
- _id: id,
339
- phaseId: targetPhaseId,
340
- })),
341
- {}
342
- );
343
- ```
344
-
345
- ### CRITICAL: activity.list() Requires Valid phaseId
346
-
347
- **Problem:** `hailer.activity.list(workflowId, '', options)` fails - empty phaseId not allowed.
348
-
349
- **Solution:** Query all phases in parallel:
350
- ```typescript
351
- const ALL_PHASES = ['phaseId1', 'phaseId2', 'phaseId3'];
352
-
353
- const phaseResults = await Promise.all(
354
- ALL_PHASES.map((phaseId) =>
355
- hailer.activity.list(workflowId, phaseId, { limit: 500 }).catch(() => [])
356
- )
357
- );
358
- const allActivities = phaseResults.flat();
359
- ```
360
-
361
- ### Phase Selection: Which Phases to Fetch
362
-
363
- **Don't blindly fetch all phases.** Consider what the user needs to see:
364
-
365
- | App Type | Phases to Fetch | Rationale |
366
- |----------|-----------------|-----------|
367
- | Sales dashboard | Active only | Don't show internal drafts |
368
- | Product manager view | Draft + Active | Need to see work-in-progress |
369
- | Archive browser | Archived only | Historical data |
370
- | Kanban board | All except Archived | Full workflow visibility |
371
-
372
- **Example: Role-based phase selection**
373
- ```typescript
374
- // Define phases per user role
375
- const PHASE_CONFIG = {
376
- sales: ['activePhaseId'],
377
- manager: ['draftPhaseId', 'activePhaseId'],
378
- admin: ['draftPhaseId', 'activePhaseId', 'archivedPhaseId'],
379
- };
380
-
381
- // Fetch based on role
382
- const userRole = 'sales'; // from app config or user check
383
- const phases = PHASE_CONFIG[userRole] || PHASE_CONFIG.sales;
384
-
385
- const results = await Promise.all(
386
- phases.map(phaseId => hailer.activity.list(workflowId, phaseId))
387
- );
388
- ```
389
-
390
- **Document phase selection in PRD:**
391
- ```markdown
392
- ## Data Access
393
- - **Product Browser**: Shows Draft + Active phases (managers need WIP visibility)
394
- - **Public Dashboard**: Active only (no internal data exposed)
395
- - **Insights**: Active only (accurate counts, no duplicates from drafts)
396
- ```
397
-
398
- ## Kanban API
399
-
400
- ```typescript
401
- // List activities grouped by phase (kanban view)
402
- const kanban = await hailer.activity.kanban.list(workflowId, options?);
403
- // Returns: { map: { [phaseId]: { activities: [...], meta: { count, ... } } } }
404
-
405
- // Load single activity in kanban format
406
- const item = await hailer.activity.kanban.load(activityId);
407
- // Returns: { activity: {...}, process: string, phase: string }
408
-
409
- // Update activity priority
410
- await hailer.activity.kanban.updatePriority(activityId, priority);
411
- ```
412
-
413
- ### KanbanListOptions
414
-
415
- ```typescript
416
- interface ActivityKanbanListOptions {
417
- filter: {
418
- user?: { uid: string; field: string };
419
- account?: string;
420
- team?: string;
421
- dates?: { field: string; start: number; end: number };
422
- };
423
- search?: string;
424
- limit?: number;
425
- skip?: number;
426
- phase?: string;
427
- includeUsers?: boolean;
428
- includeTeams?: boolean;
429
- }
430
- ```
431
-
432
- ## UI API
433
-
434
- ### Activity UI
435
-
436
- ```typescript
437
- // Open activity in sidebar
438
- await hailer.ui.activity.open(activityId, options?);
439
-
440
- type ActivityTabTypes = 'detail' | 'discussion' | 'files' | 'location' | 'linkedFrom' | 'options';
441
-
442
- // Open with specific tab
443
- await hailer.ui.activity.open(activityId, { tab: 'files' });
444
-
445
- // Open create form (returns created activity or null if cancelled)
446
- const result = await hailer.ui.activity.create(workflowId, {
447
- name?: string,
448
- fields?: { [fieldId]: value },
449
- location?: { type: 'point', data: [{ lat, lng }] }
450
- });
451
-
452
- // Bulk edit multiple activities
453
- await hailer.ui.activity.editMultiple(activityIds[]);
454
- ```
455
-
456
- **Note:** `hailer.openSidebar()` does NOT exist - use `hailer.ui.activity.open()`.
457
-
458
- ### File Upload
459
-
460
- ```typescript
461
- // Upload file to Hailer (returns file ID)
462
- const fileId = await hailer.ui.files.uploadFile(
463
- file, // File object from <input type="file">
464
- filename, // Desired filename
465
- { isPublic?: boolean }
466
- );
467
-
468
- // Then attach to activity on create:
469
- await hailer.activity.create(workflowId, [{ name: 'Doc' }], { fileIds: [fileId] });
470
- ```
471
-
472
- ### Snackbar (Toast Notifications)
473
-
474
- ```typescript
475
- // Show notification
476
- await hailer.ui.snackbar.open(text, buttonLabel, duration?);
477
-
478
- // Examples
479
- hailer.ui.snackbar.open('Saved!', 'OK');
480
- hailer.ui.snackbar.open('Deleted', 'Undo', 5000);
481
- ```
482
-
483
- ### Insight UI
484
-
485
- ```typescript
486
- await hailer.ui.insight.create(workspaceId?); // Open create dialog
487
- await hailer.ui.insight.edit(insightId); // Open edit dialog
488
- await hailer.ui.insight.delete(insightId); // Open delete confirmation
489
- await hailer.ui.insight.permission(insightId); // Open permissions dialog
490
- ```
491
-
492
- ## Insight API
493
-
494
- ```typescript
495
- // Get insight data (SQL query results)
496
- const data = await hailer.insight.data(insightId, { update?: true });
497
-
498
- // List all insights
499
- const insights = await hailer.insight.list();
500
-
501
- // Update insight
502
- const updated = await hailer.insight.update(insightId, partialUpdate);
503
- ```
504
-
505
- **Response structure:**
506
- ```typescript
507
- interface InsightData {
508
- columns: string[];
509
- rows: any[][];
510
- }
511
-
512
- interface InsightDoc {
513
- _id: string;
514
- name: string;
515
- // ... other insight properties
516
- }
517
- ```
518
-
519
- ## Workflow API
520
-
521
- ```typescript
522
- // List all workflows
523
- const workflows = await hailer.workflow.list();
524
-
525
- // Get single workflow with full schema
526
- const workflow = await hailer.workflow.get(workflowId);
527
- ```
528
-
529
- ## User API
530
-
531
- ```typescript
532
- // Get current logged-in user
533
- const me = await hailer.user.current();
534
- // Returns: { _id, email, firstname, lastname, ... }
535
-
536
- // Get specific user by ID
537
- const user = await hailer.user.get(userId);
538
-
539
- // List all workspace users (returns object keyed by user ID)
540
- const users = await hailer.user.list();
541
- ```
542
-
543
- ### Getting User Info for Personalized Greeting
544
-
545
- **The config callback does NOT include user info:**
546
- ```typescript
547
- // ❌ WRONG - config only has { fields: {} }
548
- const { config } = useHailer();
549
- const userName = config?.userName; // undefined!
550
- ```
551
-
552
- **Use hailer.user.current() instead:**
553
- ```typescript
554
- const { inside, hailer } = useHailer();
555
- const [userName, setUserName] = useState<string>('');
556
-
557
- useEffect(() => {
558
- if (!inside) return;
559
-
560
- hailer.user.current().then(user => {
561
- setUserName(user.firstname || 'there');
562
- });
563
- }, [inside]);
564
-
565
- return <Heading>Hello, {userName}!</Heading>;
566
- ```
567
-
568
- ## Workspace API
569
-
570
- ```typescript
571
- // Get current workspace
572
- const workspace = await hailer.workspace.current();
573
-
574
- // List workspaces (personal apps only)
575
- const workspaces = await hailer.workspace.list();
576
- ```
577
- </sdk-api>
578
-
579
- <field-patterns>
580
- ## Extracting Field Values
581
-
582
- ```typescript
583
- // Fields are optional and nested
584
- interface Activity {
585
- fields?: Record<string, { value: unknown }>;
586
- }
587
-
588
- // Safe extraction helper
589
- function getFieldValue<T>(activity: Activity, fieldId: string, defaultValue: T): T {
590
- return (activity.fields?.[fieldId]?.value as T) ?? defaultValue;
591
- }
592
-
593
- // Usage
594
- const name = getFieldValue(activity, 'fieldId123', '');
595
- const count = getFieldValue(activity, 'fieldId456', 0);
596
- const date = getFieldValue(activity, 'fieldId789', null);
597
- ```
598
-
599
- ## Field Types
600
-
601
- ```typescript
602
- // Text field
603
- const text = activity.fields?.['fieldId']?.value as string;
604
-
605
- // Number field
606
- const num = activity.fields?.['fieldId']?.value as number;
607
-
608
- // Date field (timestamp)
609
- const date = activity.fields?.['fieldId']?.value as number;
610
- const formatted = new Date(date).toLocaleDateString();
611
-
612
- // Enum/Select field
613
- const status = activity.fields?.['fieldId']?.value as string;
614
-
615
- // ActivityLink field (reference to another activity)
616
- // IMPORTANT: ActivityLink has nested structure, not direct values
617
- interface ActivityLinkValue {
618
- _id: string;
619
- name: string;
620
- }
621
- const linked = activity.fields?.['fieldId']?.value as ActivityLinkValue;
622
- const linkedId = linked?._id; // Get linked activity ID
623
- const linkedName = linked?.name || 'Unknown'; // Get display name
624
-
625
- // User field
626
- interface UserValue {
627
- _id: string;
628
- firstname: string;
629
- lastname: string;
630
- }
631
- const user = activity.fields?.['fieldId']?.value as UserValue;
632
- const userName = user ? `${user.firstname} ${user.lastname}` : 'Unknown';
633
- ```
634
-
635
- ## Finnish Date Parsing
636
-
637
- Insights and some fields return Finnish date strings (`dd.mm.yyyy`) instead of timestamps. These can't be sorted directly.
638
-
639
- ```typescript
640
- // ❌ WRONG - Number("03.02.2026") returns NaN
641
- dates.sort((a, b) => Number(a) - Number(b));
642
-
643
- // ✅ CORRECT - Parse Finnish dates to Date objects
644
- function parseFinnishDate(dateStr: string): Date | null {
645
- // Matches "03.02.2026" or "03.02.2026 10:00"
646
- const match = dateStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?/);
647
- if (!match) return null;
648
-
649
- const [, day, month, year, hours = '0', minutes = '0'] = match;
650
- return new Date(
651
- parseInt(year),
652
- parseInt(month) - 1, // JS months are 0-indexed
653
- parseInt(day),
654
- parseInt(hours),
655
- parseInt(minutes)
656
- );
657
- }
658
-
659
- // Sorting Finnish dates
660
- items.sort((a, b) => {
661
- const dateA = parseFinnishDate(a.dateField);
662
- const dateB = parseFinnishDate(b.dateField);
663
- if (!dateA || !dateB) return 0;
664
- return dateA.getTime() - dateB.getTime();
665
- });
666
-
667
- // Formatting back to Finnish
668
- function formatFinnishDate(date: Date): string {
669
- return date.toLocaleDateString('fi-FI'); // "3.2.2026"
670
- }
671
-
672
- function formatFinnishDateTime(date: Date): string {
673
- return date.toLocaleString('fi-FI', {
674
- day: 'numeric',
675
- month: 'numeric',
676
- year: 'numeric',
677
- hour: '2-digit',
678
- minute: '2-digit',
679
- }); // "3.2.2026 klo 10.00"
680
- }
681
- ```
682
-
683
- **When to use:**
684
- - Insight data returns formatted dates (not timestamps)
685
- - Text fields containing dates
686
- - Displaying dates to Finnish users
687
- </field-patterns>
688
-
689
- <component-templates>
690
- ## Activity Table
691
-
692
- ```typescript
693
- import { Table, Thead, Tbody, Tr, Th, Td, Box, Spinner, Text } from '@chakra-ui/react';
694
-
695
- interface Activity {
696
- _id: string;
697
- name: string;
698
- fields?: Record<string, { value: unknown }>;
699
- }
700
-
701
- interface Props {
702
- activities: Activity[];
703
- loading: boolean;
704
- columns: { fieldId: string; label: string }[];
705
- }
706
-
707
- function ActivityTable({ activities, loading, columns }: Props) {
708
- if (loading) return <Spinner />;
709
- if (activities.length === 0) return <Text>No data</Text>;
710
-
711
- return (
712
- <Table variant="simple" size="sm">
713
- <Thead>
714
- <Tr>
715
- <Th>Name</Th>
716
- {columns.map(col => (
717
- <Th key={col.fieldId}>{col.label}</Th>
718
- ))}
719
- </Tr>
720
- </Thead>
721
- <Tbody>
722
- {activities.map(activity => (
723
- <Tr key={activity._id}>
724
- <Td>{activity.name}</Td>
725
- {columns.map(col => (
726
- <Td key={col.fieldId}>
727
- {String(activity.fields?.[col.fieldId]?.value ?? '-')}
728
- </Td>
729
- ))}
730
- </Tr>
731
- ))}
732
- </Tbody>
733
- </Table>
734
- );
735
- }
736
- ```
737
-
738
- ## Activity Card
739
-
740
- ```typescript
741
- import { Box, Heading, Text, VStack, useColorModeValue } from '@chakra-ui/react';
742
-
743
- interface Props {
744
- activity: Activity;
745
- fieldId: string;
746
- fieldLabel: string;
747
- }
748
-
749
- function ActivityCard({ activity, fieldId, fieldLabel }: Props) {
750
- const bg = useColorModeValue('white', 'gray.700');
751
- const borderColor = useColorModeValue('gray.200', 'gray.600');
752
-
753
- return (
754
- <Box
755
- p={4}
756
- bg={bg}
757
- borderRadius="md"
758
- border="1px"
759
- borderColor={borderColor}
760
- >
761
- <VStack align="start" spacing={2}>
762
- <Heading size="sm">{activity.name}</Heading>
763
- <Text fontSize="sm" color="gray.500">
764
- {fieldLabel}: {String(activity.fields?.[fieldId]?.value ?? '-')}
765
- </Text>
766
- </VStack>
767
- </Box>
768
- );
769
- }
770
- ```
771
-
772
- ## Stats Card
773
-
774
- ```typescript
775
- import { Stat, StatLabel, StatNumber, StatHelpText, Box, useColorModeValue } from '@chakra-ui/react';
776
-
777
- interface Props {
778
- label: string;
779
- value: number | string;
780
- helpText?: string;
781
- }
782
-
783
- function StatsCard({ label, value, helpText }: Props) {
784
- const bg = useColorModeValue('white', 'gray.700');
785
-
786
- return (
787
- <Box p={4} bg={bg} borderRadius="md" shadow="sm">
788
- <Stat>
789
- <StatLabel>{label}</StatLabel>
790
- <StatNumber>{value}</StatNumber>
791
- {helpText && <StatHelpText>{helpText}</StatHelpText>}
792
- </Stat>
793
- </Box>
794
- );
795
- }
796
- ```
797
- </component-templates>
798
-
799
- <app-template>
800
- ## Full App Template
801
-
802
- ```typescript
803
- import { useEffect, useState } from 'react';
804
- import {
805
- Box,
806
- Heading,
807
- Text,
808
- Spinner,
809
- Table,
810
- Thead,
811
- Tbody,
812
- Tr,
813
- Th,
814
- Td,
815
- VStack,
816
- useColorModeValue,
817
- } from '@chakra-ui/react';
818
- import useHailer from './hailer/use-hailer';
819
-
820
- // Field IDs from workflow schema (provided by orchestrator)
821
- const FIELDS = {
822
- NAME_FIELD: 'fieldId123',
823
- STATUS_FIELD: 'fieldId456',
824
- } as const;
825
-
826
- interface Activity {
827
- _id: string;
828
- name: string;
829
- fields?: Record<string, { value: unknown }>;
830
- }
831
-
832
- function App() {
833
- const { inside, hailer } = useHailer();
834
- const [activities, setActivities] = useState<Activity[]>([]);
835
- const [loading, setLoading] = useState(true);
836
- const [error, setError] = useState<string | null>(null);
837
-
838
- const bg = useColorModeValue('gray.50', 'gray.800');
839
-
840
- // Fetch data when inside Hailer
841
- useEffect(() => {
842
- if (!inside) return;
843
-
844
- async function fetchData() {
845
- try {
846
- setLoading(true);
847
- const data = await hailer.activity.list(
848
- 'workflowId', // Replace with actual workflow ID
849
- 'phaseId', // Replace with actual phase ID
850
- { limit: 100 }
851
- );
852
- setActivities(data);
853
- } catch (err) {
854
- setError(err instanceof Error ? err.message : 'Failed to load data');
855
- } finally {
856
- setLoading(false);
857
- }
858
- }
859
-
860
- fetchData();
861
- }, [inside]); // IMPORTANT: [inside] not [hailer]
862
-
863
- // Early return AFTER hooks
864
- if (!inside) {
865
- return (
866
- <Box p={8} textAlign="center">
867
- <Text>Please open this app inside Hailer</Text>
868
- </Box>
869
- );
870
- }
871
-
872
- if (loading) {
873
- return (
874
- <Box p={8} textAlign="center">
875
- <Spinner size="xl" />
876
- </Box>
877
- );
878
- }
879
-
880
- if (error) {
881
- return (
882
- <Box p={8} textAlign="center">
883
- <Text color="red.500">{error}</Text>
884
- </Box>
885
- );
886
- }
887
-
888
- return (
889
- <Box p={4} bg={bg} minH="100vh">
890
- <VStack spacing={4} align="stretch">
891
- <Heading size="lg">Dashboard</Heading>
892
-
893
- <Table variant="simple" size="sm">
894
- <Thead>
895
- <Tr>
896
- <Th>Name</Th>
897
- <Th>Status</Th>
898
- </Tr>
899
- </Thead>
900
- <Tbody>
901
- {activities.map(activity => (
902
- <Tr key={activity._id}>
903
- <Td>{activity.name}</Td>
904
- <Td>{String(activity.fields?.[FIELDS.STATUS_FIELD]?.value ?? '-')}</Td>
905
- </Tr>
906
- ))}
907
- </Tbody>
908
- </Table>
909
- </VStack>
910
- </Box>
911
- );
912
- }
913
-
914
- export default App;
915
- ```
916
- </app-template>
917
-
918
- <theme-patterns>
919
- ## Hailer Theme Colors
920
-
921
- ```typescript
922
- // Dark mode support - ALWAYS use useColorModeValue
923
- const bg = useColorModeValue('white', 'gray.700');
924
- const borderColor = useColorModeValue('gray.200', 'gray.600');
925
- const textColor = useColorModeValue('gray.800', 'white');
926
- const mutedColor = useColorModeValue('gray.500', 'gray.400');
927
-
928
- // Valid color tokens (DO NOT invent tokens)
929
- // gray.50, gray.100, ..., gray.900
930
- // white, black
931
- // red.500, green.500, blue.500, yellow.500, purple.500
932
- ```
933
-
934
- ## Light/Dark Mode Safety Rules
935
-
936
- **DON'T use these patterns:**
937
- ```typescript
938
- // ❌ WRONG - brand colors may not be defined in theme
939
- color="brand.600"
940
-
941
- // ❌ WRONG - hard-coded white fails on light backgrounds
942
- <Text color="white">Always white</Text>
943
-
944
- // ❌ WRONG - assumes light mode background
945
- <Badge bg="blue.100" color="blue.800">Status</Badge>
946
- ```
947
-
948
- **DO use these patterns:**
949
- ```typescript
950
- // ✅ CORRECT - explicit colors that exist in Chakra
951
- color="blue.600"
952
-
953
- // ✅ CORRECT - adapts to color mode
954
- <Text color={useColorModeValue('gray.800', 'white')}>Adapts</Text>
955
-
956
- // ✅ CORRECT - badge adapts to mode
957
- const badgeBg = useColorModeValue('blue.100', 'blue.700');
958
- const badgeColor = useColorModeValue('blue.800', 'blue.100');
959
- <Badge bg={badgeBg} color={badgeColor}>Status</Badge>
960
- ```
961
-
962
- **Semantic tokens (define once, use everywhere):**
963
- ```typescript
964
- // In theme.ts extendTheme
965
- semanticTokens: {
966
- colors: {
967
- appBg: { _light: 'gray.50', _dark: 'gray.800' },
968
- cardBg: { _light: 'white', _dark: 'gray.700' },
969
- textPrimary: { _light: 'gray.800', _dark: 'white' },
970
- textMuted: { _light: 'gray.500', _dark: 'gray.400' },
971
- }
972
- }
973
-
974
- // Usage - no useColorModeValue needed
975
- <Box bg="appBg"><Text color="textPrimary">Clean!</Text></Box>
976
- ```
977
-
978
- ## Phase-Colored Badges and Bars
979
-
980
- When displaying phase/status colors that need to work in both modes:
981
-
982
- ```typescript
983
- import { useColorMode } from '@chakra-ui/react';
984
-
985
- // Helper for phase-colored elements
986
- function usePhaseColors(baseColor: string) {
987
- const { colorMode } = useColorMode();
988
-
989
- if (colorMode === 'light') {
990
- return {
991
- bg: `${baseColor}.100`,
992
- color: `${baseColor}.800`,
993
- borderColor: `${baseColor}.200`,
994
- };
995
- } else {
996
- return {
997
- bg: `${baseColor}.700`,
998
- color: `${baseColor}.100`,
999
- borderColor: `${baseColor}.600`,
1000
- };
1001
- }
1002
- }
1003
-
1004
- // Usage
1005
- function PhaseBadge({ phase, color }: { phase: string; color: string }) {
1006
- const colors = usePhaseColors(color);
1007
-
1008
- return (
1009
- <Badge bg={colors.bg} color={colors.color}>
1010
- {phase}
1011
- </Badge>
1012
- );
1013
- }
1014
-
1015
- // Event bar with phase color
1016
- function EventBar({ title, phaseColor }: { title: string; phaseColor: string }) {
1017
- const colors = usePhaseColors(phaseColor);
1018
-
1019
- return (
1020
- <Box
1021
- px={2}
1022
- py={1}
1023
- bg={colors.bg}
1024
- color={colors.color}
1025
- borderLeft="3px solid"
1026
- borderLeftColor={colors.borderColor}
1027
- borderRadius="sm"
1028
- >
1029
- {title}
1030
- </Box>
1031
- );
1032
- }
1033
- ```
1034
-
1035
- **Color mapping from Hailer phases:**
1036
- ```typescript
1037
- // Map Hailer phase colors to Chakra color names
1038
- const PHASE_COLOR_MAP: Record<string, string> = {
1039
- 'blue': 'blue',
1040
- 'green': 'green',
1041
- 'red': 'red',
1042
- 'yellow': 'yellow',
1043
- 'purple': 'purple',
1044
- 'orange': 'orange',
1045
- 'gray': 'gray',
1046
- };
1047
- ```
1048
-
1049
- ## Common UI Patterns
1050
-
1051
- ```typescript
1052
- // Page container
1053
- <Box p={4} bg={useColorModeValue('gray.50', 'gray.800')} minH="100vh">
1054
-
1055
- // Card
1056
- <Box p={4} bg={useColorModeValue('white', 'gray.700')} borderRadius="md" shadow="sm">
1057
-
1058
- // Section with border
1059
- <Box p={4} border="1px" borderColor={useColorModeValue('gray.200', 'gray.600')} borderRadius="md">
1060
- ```
1061
- </theme-patterns>
1062
-
1063
- <troubleshooting>
1064
- ## Workflow Permission Errors
1065
-
1066
- When SDK calls fail with permission/not-allowed errors, check **workflow configuration in Hailer** first.
1067
-
1068
- **Common symptoms:**
1069
- - `hailer.activity.move()` fails with permission error
1070
- - Phase transitions not working
1071
- - "Not allowed" errors on operations that should work
1072
-
1073
- **Cause:** Features like phase transitions must be **enabled in workflow configuration** in Hailer. The SDK can't do operations that aren't configured.
1074
-
1075
- **Debug steps:**
1076
- 1. Check Hailer UI: Workflow Settings → Phase settings
1077
- 2. Verify phase transitions are configured (`possibleNextPhase`)
1078
- 3. Verify user has permission to the workflow/phase
1079
- 4. Check if the feature (move, archive, etc.) is enabled for that phase
1080
-
1081
- **Example:** Phase move fails because `possibleNextPhase` doesn't include the target phase.
1082
- </troubleshooting>
1083
-
1084
- <build-fixes>
1085
- ## Common Build Errors
1086
-
1087
- ### Cannot find module '@hailer/app-sdk'
1088
- ```typescript
1089
- // WRONG
1090
- import { useHailer } from '@hailer/app-sdk';
1091
-
1092
- // CORRECT
1093
- import useHailer from './hailer/use-hailer';
1094
- ```
1095
-
1096
- ### has no exported member 'useHailer'
1097
- ```typescript
1098
- // WRONG - named import
1099
- import { useHailer } from './hailer/use-hailer';
1100
-
1101
- // CORRECT - default import
1102
- import useHailer from './hailer/use-hailer';
1103
- ```
1104
-
1105
- ### fields possibly undefined
1106
- ```typescript
1107
- // WRONG
1108
- const value = activity.fields[fieldId].value;
1109
-
1110
- // CORRECT
1111
- const value = activity.fields?.[fieldId]?.value;
1112
- ```
1113
-
1114
- ### Infinite re-render loop
1115
- ```typescript
1116
- // WRONG - hailer changes every render
1117
- useEffect(() => { ... }, [hailer]);
1118
-
1119
- // CORRECT - inside is stable
1120
- useEffect(() => { ... }, [inside]);
1121
- ```
1122
-
1123
- ### Hooks order error
1124
- ```typescript
1125
- // WRONG - early return before hook
1126
- if (!inside) return <Text>Error</Text>;
1127
- const [data, setData] = useState([]); // Error!
1128
-
1129
- // CORRECT - hooks first, then early return
1130
- const [data, setData] = useState([]);
1131
- if (!inside) return <Text>Error</Text>;
1132
- ```
1133
-
1134
- ### Hooks inside map() or conditionals
1135
- ```typescript
1136
- // WRONG - hook in map
1137
- {items.map((item) => (
1138
- <Tr _hover={{ bg: useColorModeValue('gray.50', 'gray.600') }}>
1139
- ))}
1140
-
1141
- // CORRECT - hook at top level
1142
- const hoverBg = useColorModeValue('gray.50', 'gray.600');
1143
- {items.map((item) => (
1144
- <Tr _hover={{ bg: hoverBg }}>
1145
- ))}
1146
- ```
1147
-
1148
- ### hailer.activity.move is not a function
1149
- ```typescript
1150
- // ❌ WRONG - activity.move() DOES NOT EXIST (common mistake)
1151
- // await hailer.activity.move(activityId, newPhaseId); // This will fail!
1152
-
1153
- // CORRECT - use activity.update() with phaseId
1154
- await hailer.activity.update([
1155
- {
1156
- _id: activityId,
1157
- phaseId: newPhaseId,
1158
- },
1159
- ], {});
1160
- ```
1161
-
1162
- **Explanation:** Phase transitions in Hailer are done via the `update()` method by setting the `phaseId` field. There is no separate `move()` method in the SDK.
1163
-
1164
- ### Routing: HashRouter vs BrowserRouter
1165
-
1166
- **Most Hailer apps don't need routing at all.** Use state-based page switching:
1167
- ```typescript
1168
- const [page, setPage] = useState('home');
1169
- {page === 'home' && <HomePage />}
1170
- {page === 'settings' && <SettingsPage />}
1171
- ```
1172
-
1173
- **If you need routing** (bookmarkable URLs, back button, public apps):
1174
- ```typescript
1175
- // ✅ USE HashRouter - works in iframe, no server config
1176
- import { createHashRouter, RouterProvider } from 'react-router-dom';
1177
-
1178
- const router = createHashRouter([
1179
- { path: '/', element: <HomePage /> },
1180
- { path: '/settings', element: <SettingsPage /> },
1181
- ]);
1182
-
1183
- // URLs look like: yourapp.com/#/settings
1184
- ```
1185
-
1186
- ```typescript
1187
- // ⚠️ AVOID BrowserRouter in iframe apps
1188
- // Can cause "No routes matched location /index.html" errors
1189
- // because iframe URL contains /index.html path
1190
- ```
1191
-
1192
- **When to use routing:**
1193
- - Public apps with shareable URLs
1194
- - Complex multi-section apps (like hailer-admin)
1195
- - Apps where back button navigation matters
1196
- </build-fixes>
1197
-
1198
- <sdk-crud>
1199
- ## SDK Create/Update Formats
1200
-
1201
- ### activity.create() Format
1202
-
1203
- **Signature:** `hailer.activity.create(workflowId, activities[], options)`
1204
-
1205
- **CRITICAL:** Takes array of activities, raw field values (not wrapped).
1206
-
1207
- ```typescript
1208
- await hailer.activity.create(WORKFLOW_ID, [
1209
- {
1210
- name: 'Activity name',
1211
- fields: {
1212
- [FIELD_ID]: 'string or number value', // NOT wrapped in { value: ... }
1213
- [ACTIVITYLINK_FIELD]: 'linkedActivityId', // Just the ID, not { _id, name }
1214
- },
1215
- },
1216
- ], {});
1217
- ```
1218
-
1219
- **Common mistakes:**
1220
- - Passing single object instead of array
1221
- - Wrapping field values in `{ value: ... }`
1222
- - Passing `{ _id, name }` for activitylinks instead of just ID
1223
-
1224
- ### activity.update() Format
1225
-
1226
- **Signature:** `hailer.activity.update(activities[], options)`
1227
-
1228
- ```typescript
1229
- await hailer.activity.update([
1230
- {
1231
- _id: 'activityId',
1232
- name: 'New name', // optional
1233
- fields: {
1234
- [FIELD_ID]: newValue,
1235
- },
1236
- phaseId: 'newPhaseId', // optional - move to different phase
1237
- },
1238
- ], {});
1239
- ```
1240
- </sdk-crud>
1241
-
1242
- <file-structure>
1243
- ## Required Files
1244
-
1245
- ```
1246
- src/
1247
- App.tsx # Main component (EDIT THIS)
1248
- main.tsx # Entry point (NEVER EDIT)
1249
- hailer/
1250
- use-hailer.ts # SDK hook (generated)
1251
- types/
1252
- index.ts # Type definitions (CREATE)
1253
- utils/
1254
- fields.ts # Field helpers (CREATE)
1255
- constants/
1256
- fields.ts # Field ID constants (CREATE)
1257
- ```
1258
-
1259
- ## Constants File Pattern
1260
-
1261
- ```typescript
1262
- // src/constants/fields.ts
1263
- export const WORKFLOW_ID = 'workflowId123';
1264
- export const PHASE_ID = 'phaseId456';
1265
-
1266
- export const FIELDS = {
1267
- NAME: 'fieldId001',
1268
- STATUS: 'fieldId002',
1269
- DATE: 'fieldId003',
1270
- } as const;
1271
- ```
1272
-
1273
- ## Types File Pattern
1274
-
1275
- ```typescript
1276
- // src/types/index.ts
1277
- export interface Activity {
1278
- _id: string;
1279
- name: string;
1280
- fields?: Record<string, { value: unknown }>;
1281
- created?: number;
1282
- updated?: number;
1283
- }
1284
-
1285
- export interface ActivityLinkValue {
1286
- _id: string;
1287
- name: string;
1288
- }
1289
-
1290
- export interface UserValue {
1291
- _id: string;
1292
- firstname: string;
1293
- lastname: string;
1294
- }
1295
- ```
1296
- </file-structure>
1297
-
1298
- <app-manifest>
1299
- ## App Manifest Configuration
1300
-
1301
- The `manifest.json` file in app root defines configurable settings exposed in Hailer UI.
1302
-
1303
- ### config.fields Structure
1304
-
1305
- ```json
1306
- {
1307
- "name": "My App",
1308
- "version": "1.0.0",
1309
- "config": {
1310
- "fields": {
1311
- "defaultView": {
1312
- "type": "string",
1313
- "label": "Default View",
1314
- "default": "month"
1315
- },
1316
- "slotMinTime": {
1317
- "type": "string",
1318
- "label": "Day Start Time",
1319
- "default": "08:00"
1320
- },
1321
- "showWeekends": {
1322
- "type": "boolean",
1323
- "label": "Show Weekends",
1324
- "default": true
1325
- },
1326
- "itemsPerPage": {
1327
- "type": "number",
1328
- "label": "Items Per Page",
1329
- "default": 25
1330
- },
1331
- "enabledFeatures": {
1332
- "type": "array",
1333
- "label": "Enabled Features",
1334
- "default": ["search", "export"]
1335
- }
1336
- }
1337
- }
1338
- }
1339
- ```
1340
-
1341
- ### Field Types
1342
-
1343
- | Type | Description | Example Default |
1344
- |------|-------------|-----------------|
1345
- | `string` | Text value | `"month"` |
1346
- | `number` | Numeric value | `25` |
1347
- | `boolean` | True/false toggle | `true` |
1348
- | `array` | List of values | `["a", "b"]` |
1349
-
1350
- ### Accessing Config in App
1351
-
1352
- Config values come through HailerApi callback when app loads inside Hailer:
1353
-
1354
- ```typescript
1355
- const { inside, hailer, config } = useHailer();
1356
-
1357
- // Access configured values
1358
- const defaultView = config?.defaultView ?? 'month';
1359
- const showWeekends = config?.showWeekends ?? true;
1360
- ```
1361
-
1362
- ### Best Practices
1363
-
1364
- **DO expose in config.fields:**
1365
- - User-customizable options (default views, display preferences)
1366
- - Workflow/phase IDs that vary per installation
1367
- - Feature toggles
1368
-
1369
- **DON'T expose in config.fields:**
1370
- - Values hardcoded in app code (wastes config UI space)
1371
- - Sensitive data (use environment variables)
1372
- - Internal constants that users shouldn't change
1373
-
1374
- **RULE:** If a config field is defined but the app ignores it, remove it from manifest.
1375
- </app-manifest>
1376
-
1377
- <public-api>
1378
- ## Public API (Apps Outside Hailer)
1379
-
1380
- For apps that run standalone (outside Hailer iframe) without authentication:
1381
-
1382
- ```typescript
1383
- // Public insight data
1384
- const data = await hailer.public.insight.data(insightKey);
1385
- const objects = await hailer.public.insight.dataAsObject(insightKey);
1386
-
1387
- // Public forms
1388
- const formData = await hailer.public.form.data(formsKey);
1389
- const result = await hailer.public.form.submit(formsKey, formData);
1390
-
1391
- // Public app config
1392
- const config = await hailer.public.app.config();
1393
-
1394
- // Public products
1395
- const products = await hailer.public.product.list(filter?, options?);
1396
- const product = await hailer.public.product.get(productId);
1397
- ```
1398
-
1399
- **When to use Public API:**
1400
- - Building external-facing apps (not in Hailer iframe)
1401
- - Public dashboards using insight keys
1402
- - Public form submissions
1403
- - No user authentication available
1404
-
1405
- **Note:** Public APIs use keys (not IDs) and don't require authentication.
1406
- </public-api>
1407
-
1408
- <sdk-reference>
1409
- ## SDK Type Definitions (For Edge Cases)
1410
-
1411
- The skill covers common SDK methods. For less common APIs, read the type definitions in any Hailer app project:
1412
-
1413
- ```
1414
- node_modules/@hailer/app-sdk/lib/modules/
1415
- ├── activity.d.ts - list, get, create, update, remove
1416
- ├── activity/kanban.d.ts - kanban.list, load, updatePriority
1417
- ├── user.d.ts - current, get, list
1418
- ├── ui.d.ts - snackbar, activity, insight, files
1419
- ├── ui/activity.d.ts - open, create, editMultiple
1420
- ├── ui/snackbar.d.ts - open
1421
- ├── ui/files.d.ts - uploadFile
1422
- ├── insight.d.ts - data, list, update
1423
- ├── process.d.ts - list, get (workflow API uses this)
1424
- ├── workspace.d.ts - current, list, product methods
1425
- ├── permission.d.ts - map
1426
- ├── app.d.ts - config.update, product methods
1427
- └── public/ - insight, form, product (no auth)
1428
- ```
1429
-
1430
- **When to check types:**
1431
- - Method not documented here → read the relevant `.d.ts` file
1432
- - Need method signature details → types are the source of truth
1433
- - New SDK version → types show what's available
1434
- </sdk-reference>