@hogsend/cli 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "tsup": "^8.5.1",
35
35
  "tsx": "^4.22.4",
36
36
  "vitest": "^4.1.7",
37
- "@hogsend/studio": "^0.18.0",
37
+ "@hogsend/studio": "^0.19.0",
38
38
  "@repo/typescript-config": "0.0.0"
39
39
  },
40
40
  "engines": {
@@ -44,8 +44,8 @@
44
44
  "@clack/prompts": "^1.5.0",
45
45
  "better-auth": "^1.6.11",
46
46
  "picocolors": "^1.1.1",
47
- "@hogsend/db": "^0.18.0",
48
- "@hogsend/engine": "^0.18.0"
47
+ "@hogsend/db": "^0.19.0",
48
+ "@hogsend/engine": "^0.19.0"
49
49
  },
50
50
  "scripts": {
51
51
  "prebuild": "node scripts/bundle-studio.mjs",
@@ -149,7 +149,10 @@ orchestration:
149
149
  - **`getPostHog()`** — `import { getPostHog } from "@hogsend/engine"` for the raw
150
150
  PostHog service (a fire-and-forget escape hatch). For fanning lifecycle data out
151
151
  to product/data tools, prefer an outbound DESTINATION (see above) — it delivers
152
- durably and is vendor-neutral.
152
+ durably and is vendor-neutral. Note: capture and `$set` person WRITES use the
153
+ `phc_` project key; person READS (`getPersonProperties`) additionally need
154
+ `POSTHOG_PERSONAL_API_KEY` (the project key is write-only by PostHog's design)
155
+ and soft-fail to `{}` without it.
153
156
  - **SMS / push / Slack** — plain functions you import, never on `ctx`.
154
157
  - There is **no `ctx.db`, no `ctx.sendEmail`, no `ctx.hatchet`** surfaced to
155
158
  consumer journeys. If you reach for one of those, you are modelling it wrong —
@@ -73,10 +73,19 @@ dashboard at `http://localhost:8888`, login `admin@example.com` / `Admin123!!`.)
73
73
  All commented-out in `.env.example` — add only what you use:
74
74
 
75
75
  ```bash
76
- # PostHog person properties + event capture (no-op if unset).
76
+ # PostHog event capture + person property WRITES (no-op if unset).
77
77
  POSTHOG_API_KEY=phc_...
78
78
  POSTHOG_HOST=https://us.i.posthog.com
79
79
 
80
+ # PostHog person property READS (timezone resolution, property conditions).
81
+ # The phc_ key is write-only by PostHog's design — reads need a PERSONAL API
82
+ # key scoped person:read. Without it reads soft-fail to contact properties.
83
+ POSTHOG_PERSONAL_API_KEY=...
84
+ # Project id for the private API — discovered automatically when unset.
85
+ # POSTHOG_PROJECT_ID=12345
86
+ # Private API host — derived (eu.i.posthog.com → eu.posthog.com) when unset.
87
+ # POSTHOG_PRIVATE_HOST=https://us.posthog.com
88
+
80
89
  # Verify incoming PostHog webhooks (POST /v1/webhooks/posthog).
81
90
  POSTHOG_WEBHOOK_SECRET=...
82
91
 
@@ -92,6 +101,12 @@ Notes:
92
101
 
93
102
  - **PostHog is fully optional.** Without `POSTHOG_API_KEY`, person-property
94
103
  fetches and event captures are no-ops — journeys still run.
104
+ - **Two PostHog credentials, by PostHog's design.** The `phc_` project key is
105
+ public (it ships in browser bundles) so PostHog makes it WRITE-only: capture
106
+ and `$set` person writes work, reads never will. Person READS (per-user
107
+ timezone resolution) need `POSTHOG_PERSONAL_API_KEY` — a personal API key
108
+ scoped `person:read`, created in PostHog → Settings → Personal API keys.
109
+ `hogsend doctor` warns when capture is configured without it.
95
110
  - **Webhook secrets are per-source.** Only set the secret for a webhook source
96
111
  you've actually registered (see the consumer's `src/webhook-sources`).
97
112
  - **`ADMIN_API_KEY` gates `/v1/admin/*`.** Set it in prod if you want to drive
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
+ import { loadDotEnv } from "../lib/config.js";
2
3
  import { isHttpError } from "../lib/http.js";
3
4
  import { color } from "../lib/output.js";
4
5
  import { skillsStaleness } from "../lib/skills.js";
@@ -22,6 +23,34 @@ function skillsNudge(ctx: CommandContext): void {
22
23
  );
23
24
  }
24
25
 
26
+ /**
27
+ * Best-effort nudge: PostHog capture configured (`POSTHOG_API_KEY` in the
28
+ * cwd's `.env` or process env) without `POSTHOG_PERSONAL_API_KEY` means
29
+ * person READS are silently disabled — the phc_ project key is write-only by
30
+ * PostHog's design, so timezone resolution falls back to contact properties.
31
+ * Warn-not-fail: capture and person writes still work.
32
+ */
33
+ function analyticsNudge(ctx: CommandContext): void {
34
+ if (ctx.json) return;
35
+ const dotenv = loadDotEnv(process.cwd());
36
+ const captureKey = process.env.POSTHOG_API_KEY ?? dotenv.POSTHOG_API_KEY;
37
+ const personalKey =
38
+ process.env.POSTHOG_PERSONAL_API_KEY ?? dotenv.POSTHOG_PERSONAL_API_KEY;
39
+ if (!captureKey || personalKey) return;
40
+ ctx.out.note(
41
+ [
42
+ "POSTHOG_API_KEY is set without POSTHOG_PERSONAL_API_KEY — person",
43
+ "property READS are disabled (the phc_ project key is write-only by",
44
+ "PostHog's design), so per-user timezone resolution falls back to",
45
+ "contact properties. Capture and person WRITES are unaffected.",
46
+ "",
47
+ `Fix: create a personal API key scoped ${color.cyan("person:read")} and set ${color.cyan("POSTHOG_PERSONAL_API_KEY")}.`,
48
+ `Docs: ${color.cyan("https://hogsend.com/docs/guides/analytics-access")}`,
49
+ ].join("\n"),
50
+ "PostHog person reads disabled",
51
+ );
52
+ }
53
+
25
54
  const usage = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
26
55
 
27
56
  Probe a running Hogsend instance via GET /v1/health and report its health:
@@ -221,6 +250,7 @@ async function run(ctx: CommandContext): Promise<void> {
221
250
  ctx.out.note(lines.join("\n"), "Doctor");
222
251
 
223
252
  skillsNudge(ctx);
253
+ analyticsNudge(ctx);
224
254
 
225
255
  if (ok) {
226
256
  ctx.out.outro(color.green("doctor: ok"));