@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
@@ -0,0 +1,364 @@
1
+ ---
2
+ name: sdk-insight-calculations
3
+ description: Named calculation recipes for Hailer insights — weighted sums, CASE bucketing, conditional counts, percentage-of-total, conversion rates, date truncation, age-since, top-N, cross-join for zero-activity rows, and Finnish DST offset. Load when designing insights that do more than plain SUM/COUNT/GROUP BY. Use alongside sdk-insight-queries (SQL mechanics) and insight-join-patterns (cross-workflow JOINs).
4
+ ---
5
+
6
+ # Insight Calculation Recipes
7
+
8
+ A library of named calculation patterns you can drop into insight SQL. Each recipe includes when to use it, a working query, and common mistakes to avoid.
9
+
10
+ **Always validate** with `mcp__hailer__run_insight` before committing. Many recipes depend on function field materialization or SQL dialect quirks only visible with real rows.
11
+
12
+ ---
13
+
14
+ ## Recipe 1 — Weighted Sum by Category
15
+
16
+ **When to use:** Forecast a pipeline by phase probability, or any metric where each row's contribution depends on a category column.
17
+
18
+ ```sql
19
+ SELECT
20
+ COALESCE(SUM(d.value * CASE d.phaseId
21
+ WHEN '${Deals_PhaseIds.new_abc}' THEN 0.10
22
+ WHEN '${Deals_PhaseIds.qualified_def}' THEN 0.25
23
+ WHEN '${Deals_PhaseIds.proposal_ghi}' THEN 0.50
24
+ WHEN '${Deals_PhaseIds.negotiation_jkl}' THEN 0.75
25
+ WHEN '${Deals_PhaseIds.won_mno}' THEN 1.00
26
+ ELSE 0
27
+ END), 0) AS "Ennustearvo",
28
+ COUNT(*) AS "Kauppoja"
29
+ FROM deals d
30
+ WHERE d.phaseId != '${Deals_PhaseIds.lost_pqr}'
31
+ ```
32
+
33
+ **Common mistakes:**
34
+ - Forgetting `COALESCE` on outer SUM → null if no rows
35
+ - Matching on `phaseName` instead of `phaseId` → breaks when someone renames a phase
36
+ - Missing `ELSE 0` → unknown phases contribute NULL and poison the SUM
37
+
38
+ ---
39
+
40
+ ## Recipe 2 — CASE Bucketing
41
+
42
+ **When to use:** Group a continuous numeric column into labeled bins (deal size tiers, revenue brackets, response-time buckets).
43
+
44
+ ```sql
45
+ SELECT
46
+ CASE
47
+ WHEN d.value < 1000 THEN '1_small'
48
+ WHEN d.value < 10000 THEN '2_mid'
49
+ WHEN d.value < 100000 THEN '3_large'
50
+ ELSE '4_enterprise'
51
+ END AS bucket,
52
+ COUNT(*) AS "Kauppoja",
53
+ COALESCE(SUM(d.value), 0) AS "Yhteensä"
54
+ FROM deals d
55
+ GROUP BY bucket
56
+ ORDER BY bucket
57
+ ```
58
+
59
+ **Common mistakes:**
60
+ - Unsorted bucket labels → add sort prefix (`1_small`, `2_mid`) so ORDER BY bucket gives natural order
61
+ - Bucketing a NULL-able field — add `WHEN d.value IS NULL THEN '0_unknown'` as first branch
62
+
63
+ ---
64
+
65
+ ## Recipe 3 — Conditional Count
66
+
67
+ **When to use:** Count rows matching a condition **within** a broader group — e.g., "won deals per owner, alongside total deals per owner."
68
+
69
+ ```sql
70
+ SELECT
71
+ COALESCE(u.name, 'Unassigned') AS "Omistaja",
72
+ COUNT(*) AS "Kauppoja yhteensä",
73
+ COUNT(CASE WHEN d.phaseId = '${Deals_PhaseIds.won_mno}' THEN 1 END) AS "Voitettu",
74
+ COUNT(CASE WHEN d.phaseId = '${Deals_PhaseIds.lost_pqr}' THEN 1 END) AS "Hävitty"
75
+ FROM deals d
76
+ LEFT JOIN user u ON u._id = d.owner
77
+ GROUP BY "Omistaja"
78
+ ORDER BY "Kauppoja yhteensä" DESC
79
+ ```
80
+
81
+ **Key insight:** `COUNT(CASE WHEN ... THEN 1 END)` counts only matched rows (NULL from non-matches is ignored by COUNT).
82
+
83
+ **Common mistake:** `COUNT(CASE WHEN ... THEN 1 ELSE 0 END)` — ELSE 0 makes it count all rows. Drop the ELSE branch.
84
+
85
+ ---
86
+
87
+ ## Recipe 4 — Percentage of Total
88
+
89
+ **When to use:** Show each row's share of the group-wide sum.
90
+
91
+ ```sql
92
+ SELECT
93
+ d.phaseName AS "Vaihe",
94
+ COALESCE(SUM(d.value), 0) AS "Arvo",
95
+ ROUND(
96
+ 100.0 * SUM(d.value) / NULLIF((SELECT SUM(value) FROM deals), 0),
97
+ 1
98
+ ) AS "% kokonaisarvosta"
99
+ FROM deals d
100
+ GROUP BY d.phaseName
101
+ ORDER BY "Arvo" DESC
102
+ ```
103
+
104
+ **Key parts:**
105
+ - `100.0 *` forces float division (integer division truncates to 0%)
106
+ - `NULLIF(denominator, 0)` prevents division-by-zero
107
+ - Correlated subquery runs once for all rows
108
+
109
+ ---
110
+
111
+ ## Recipe 5 — Conversion Rate / Win Rate
112
+
113
+ **When to use:** Ratio of "ever reached phase X" count to "ever reached phase Y" count.
114
+
115
+ **Use `meta: 'phaseLastMove'` — one declared source field per phase.**
116
+
117
+ ```javascript
118
+ // Source declaration
119
+ sources: [{
120
+ name: 'offers',
121
+ workflowId: WorkflowIds.offers,
122
+ fields: [
123
+ { name: 'id', meta: '_id' },
124
+ { name: 'owner', fieldId: Offers_FieldIds.owner_abc },
125
+ { name: 'plm_sent', meta: 'phaseLastMove', phaseId: Offers_PhaseIds.sent_def },
126
+ { name: 'plm_accepted',meta: 'phaseLastMove', phaseId: Offers_PhaseIds.accepted_ghi },
127
+ ]
128
+ }]
129
+ ```
130
+
131
+ ```sql
132
+ SELECT
133
+ COALESCE(u.name, 'Unassigned') AS "Omistaja",
134
+ COUNT(CASE WHEN plm_sent IS NOT NULL THEN 1 END) AS "Lähetetty",
135
+ COUNT(CASE WHEN plm_accepted IS NOT NULL THEN 1 END) AS "Hyväksytty",
136
+ ROUND(
137
+ 100.0 * COUNT(CASE WHEN plm_accepted IS NOT NULL THEN 1 END)
138
+ / NULLIF(COUNT(CASE WHEN plm_sent IS NOT NULL THEN 1 END), 0),
139
+ 1
140
+ ) AS "Hyväksymisprosentti"
141
+ FROM offers
142
+ LEFT JOIN user u ON u._id = offers.owner
143
+ GROUP BY "Omistaja"
144
+ ORDER BY "Hyväksymisprosentti" DESC NULLS LAST
145
+ ```
146
+
147
+ **Why `plm_X IS NOT NULL`, not `phaseId = X`:** An offer that went Sent → Accepted → Archived has `phaseId = Archived` now, but `plm_accepted` is non-null. `phaseId = Accepted` only catches activities currently parked there — it misses every accepted offer that moved on.
148
+
149
+ **Common mistakes:**
150
+ - Using `CASE WHEN phaseId = X` for conversion — misses activities that passed through X
151
+ - Trying `movedTo<PhaseName>` — that column doesn't exist; declare a `phaseLastMove` source field per phase instead
152
+
153
+ ---
154
+
155
+ ## Recipe 6 — Date Truncation for Time Series
156
+
157
+ **When to use:** Group rows by month/week/quarter/year to produce trend charts.
158
+
159
+ **For a millisecond timestamp** (activity meta like `d.created`):
160
+
161
+ ```sql
162
+ SELECT
163
+ strftime('%Y-%m', d.created / 1000, 'unixepoch') AS month,
164
+ COUNT(*) AS deal_count,
165
+ COALESCE(SUM(d.value), 0) AS revenue
166
+ FROM deals d
167
+ WHERE d.phaseId = '${Deals_PhaseIds.won_mno}'
168
+ GROUP BY month
169
+ ORDER BY month
170
+ ```
171
+
172
+ **For a seconds timestamp** (most custom date fields):
173
+
174
+ ```sql
175
+ strftime('%Y-%m', d.closeDate, 'unixepoch') AS month
176
+ ```
177
+
178
+ **Truncation format codes:**
179
+ - `'%Y'` → year (e.g. `2026`)
180
+ - `'%Y-%m'` → month (e.g. `2026-04`)
181
+ - `'%Y-%W'` → ISO week
182
+ - `'%Y-%m-%d'` → day
183
+
184
+ **Common mistakes:**
185
+ - Forgetting to divide by 1000 when field is ms → year 57259
186
+ - Forgetting the `'unixepoch'` modifier → garbage output
187
+ - Forgetting ORDER BY — time-series charts need sorted output
188
+
189
+ ---
190
+
191
+ ## Recipe 7 — Age / Days-Since
192
+
193
+ **When to use:** "Stale deals" reports without relying on a function field.
194
+
195
+ **"Now" in SQLite is `strftime('%s', 'now')`** — returns seconds. `NOW()` and `UNIX_TIMESTAMP()` are not available.
196
+
197
+ **If comparing to a millisecond field** (e.g. `meta.created`):
198
+
199
+ ```sql
200
+ SELECT
201
+ d.name AS deal,
202
+ CAST((strftime('%s', 'now') * 1000 - d.created) / 86400000 AS INTEGER) AS days_old
203
+ FROM deals d
204
+ WHERE d.phaseId NOT IN ('${Deals_PhaseIds.won_mno}', '${Deals_PhaseIds.lost_pqr}')
205
+ AND (strftime('%s', 'now') * 1000 - d.created) > (30 * 86400000)
206
+ ORDER BY days_old DESC
207
+ ```
208
+
209
+ **If comparing to a seconds field** (custom date field):
210
+
211
+ ```sql
212
+ CAST((strftime('%s', 'now') - d.closeDate) / 86400 AS INTEGER) AS days_until_close
213
+ ```
214
+
215
+ **strftime('%s','now') returns TEXT — affects MIN/MAX and comparisons.** Always wrap in `CAST(... AS INTEGER)` before mixing into numeric MIN/MAX/comparison.
216
+
217
+ ```sql
218
+ -- Dangerous — string ordering, wrong result
219
+ MIN(strftime('%s','now'), 9999999999)
220
+
221
+ -- Safe
222
+ MIN(CAST(strftime('%s','now') AS INTEGER), 9999999999)
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Recipe 8 — Top-N per Group
228
+
229
+ **When to use:** "Top 3 deals per owner", "Largest 5 accounts per industry."
230
+
231
+ ```sql
232
+ SELECT owner, deal, value
233
+ FROM (
234
+ SELECT
235
+ COALESCE(u.name, 'Unassigned') AS owner,
236
+ d.name AS deal,
237
+ d.value,
238
+ ROW_NUMBER() OVER (PARTITION BY d.owner ORDER BY d.value DESC) AS rn
239
+ FROM deals d
240
+ LEFT JOIN user u ON u._id = d.owner
241
+ ) ranked
242
+ WHERE rn <= 3
243
+ ORDER BY owner, value DESC
244
+ ```
245
+
246
+ **Fallback — correlated subquery if no window functions:**
247
+
248
+ ```sql
249
+ SELECT COALESCE(u.name, 'Unassigned') AS owner, d.name AS deal, d.value
250
+ FROM deals d
251
+ LEFT JOIN user u ON u._id = d.owner
252
+ WHERE (
253
+ SELECT COUNT(*) FROM deals d2 WHERE d2.owner = d.owner AND d2.value > d.value
254
+ ) < 3
255
+ ORDER BY owner, d.value DESC
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Recipe 9 — Function Fields in WHERE/SELECT
261
+
262
+ Function field values are materialized on each activity and queryable in all SQL positions.
263
+
264
+ | Usage | Works? |
265
+ |---|---|
266
+ | `SELECT d.my_ff` | Yes — returns stored FF value (NULL if FF returned null) |
267
+ | `WHERE d.my_ff > 30` | Yes |
268
+ | `ORDER BY d.my_ff` | Yes |
269
+ | `SUM(d.my_ff)` / `AVG(d.my_ff)` | Yes |
270
+
271
+ **Type note:** FFs declared numeric arrive as `real` (float). If a value is `0.0`, the run_insight text renderer may display it as blank — use `CAST(d.my_ff AS TEXT)` to confirm.
272
+
273
+ **Inline re-computation is still recommended when:**
274
+ - You don't trust the FF compute to be current (e.g. after bulk phase moves, FFs may be stale until next write)
275
+ - The calculation is simple enough to express inline
276
+
277
+ ---
278
+
279
+ ## Recipe 10 — Helsinki / Finnish DST Offset
280
+
281
+ **When to use:** Any insight that formats timestamps to local Finnish time.
282
+
283
+ **Offsets:** EEST (UTC+3, late March → late October), EET (UTC+2, the rest of the year).
284
+
285
+ **Critical — wrap all `strftime` DST comparisons in `CAST(... AS INTEGER)`** — `strftime('%s', ...)` returns TEXT; comparing a numeric unix column against it inside CASE falls back to string ordering.
286
+
287
+ ```sql
288
+ WITH dst_bounds AS (
289
+ SELECT
290
+ CAST(strftime('%s',
291
+ date(strftime('%Y', ts, 'unixepoch') || '-03-31', 'weekday 0', '-7 days')
292
+ || ' 03:00:00'
293
+ ) AS INTEGER) AS summer_start,
294
+ CAST(strftime('%s',
295
+ date(strftime('%Y', ts, 'unixepoch') || '-10-31', 'weekday 0', '-7 days')
296
+ || ' 04:00:00'
297
+ ) AS INTEGER) AS summer_end
298
+ FROM src LIMIT 1
299
+ )
300
+ SELECT
301
+ strftime(
302
+ '%d.%m.%Y %H:%M', ts, 'unixepoch',
303
+ CASE
304
+ WHEN CAST(ts AS INTEGER) >= (SELECT summer_start FROM dst_bounds)
305
+ AND CAST(ts AS INTEGER) < (SELECT summer_end FROM dst_bounds)
306
+ THEN '+3 hours'
307
+ ELSE '+2 hours'
308
+ END
309
+ ) AS local_time
310
+ FROM src
311
+ ```
312
+
313
+ **For millisecond-unit timestamps:** divide by 1000 before all `strftime` calls.
314
+
315
+ ---
316
+
317
+ ## Recipe 11 — Show All Entities Even with No Activity (Cross-Join Pattern)
318
+
319
+ **When to use:** You want a complete grid — every entity × every time period — even when some combinations have zero activity. Without this, GROUP BY silently omits zero-count rows.
320
+
321
+ ```sql
322
+ WITH entities AS (
323
+ SELECT _id AS entity_id, activityName AS entity_name
324
+ FROM entity_workflow
325
+ ),
326
+ periods AS (
327
+ SELECT DISTINCT strftime('%Y-%m', booking_start, 'unixepoch') AS period
328
+ FROM bookings
329
+ ),
330
+ aggregated AS (
331
+ SELECT entity_link AS entity_id, strftime('%Y-%m', booking_start, 'unixepoch') AS period, COUNT(*) AS booking_count
332
+ FROM bookings
333
+ GROUP BY entity_link, period
334
+ )
335
+ SELECT
336
+ e.entity_id, e.entity_name, p.period,
337
+ COALESCE(a.booking_count, 0) AS booking_count
338
+ FROM entities e
339
+ CROSS JOIN periods p
340
+ LEFT JOIN aggregated a ON a.entity_id = e.entity_id AND a.period = p.period
341
+ ORDER BY e.entity_name, p.period
342
+ ```
343
+
344
+ **Key mechanics:**
345
+ - `CROSS JOIN` produces every entity × period combination
346
+ - `LEFT JOIN aggregated` brings in actual data; unmatched combinations stay as NULL
347
+ - `COALESCE(..., 0)` converts NULL to 0
348
+
349
+ **Diagnostic reminder:** if row count is lower than expected and data "feels like it should be there", check whether you're using INNER JOIN or plain GROUP BY — both silently drop zero-activity entities.
350
+
351
+ ---
352
+
353
+ ## Debugging Checklist (Silent-Wrong-Answer Mode)
354
+
355
+ Before trusting an insight's numbers:
356
+
357
+ 1. **Row count check** — `SELECT COUNT(*) FROM wf` with no filters matches expected workspace count
358
+ 2. **Spot-check a known row** — pick one activity you know the value of, `WHERE _id = 'known'`, assert returned value matches
359
+ 3. **Denominator exists** — any `X / Y` has `NULLIF(Y, 0)` on Y
360
+ 4. **Float division** — any percentage multiplies by `100.0` not `100`
361
+ 5. **NULL handling** — outer aggregates wrapped in `COALESCE(..., 0)` or similar default
362
+ 6. **Phase IDs not names** — all phase filters use `phaseId` (rename-safe)
363
+ 7. **Preview with real data** — never ship an insight whose `run_insight` output you haven't read row by row
364
+ 8. **Row count lower than expected?** — check for INNER JOIN or plain GROUP BY silently dropping zero-activity entities; use CROSS JOIN + LEFT JOIN (Recipe 11)