@hogsend/cli 0.0.1 → 0.2.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.
Files changed (63) hide show
  1. package/dist/bin.js +2238 -75
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +9 -1
  4. package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
  5. package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
  6. package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
  7. package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
  8. package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
  9. package/skills/hogsend-authoring-emails/SKILL.md +68 -0
  10. package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
  11. package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
  12. package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
  13. package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
  14. package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
  15. package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +93 -0
  16. package/skills/hogsend-authoring-journeys/references/journey-context.md +110 -0
  17. package/skills/hogsend-authoring-journeys/references/journey-meta.md +142 -0
  18. package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
  19. package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
  20. package/skills/hogsend-cli/SKILL.md +81 -0
  21. package/skills/hogsend-cli/references/debug-a-journey.md +66 -0
  22. package/skills/hogsend-cli/references/manage-journeys.md +53 -0
  23. package/skills/hogsend-cli/references/query-stats.md +66 -0
  24. package/skills/hogsend-cli/references/setup-local.md +52 -0
  25. package/skills/hogsend-conditions/SKILL.md +70 -0
  26. package/skills/hogsend-conditions/references/condition-types.md +251 -0
  27. package/skills/hogsend-conditions/references/durations.md +90 -0
  28. package/skills/hogsend-conditions/references/examples.md +188 -0
  29. package/skills/hogsend-database/SKILL.md +70 -0
  30. package/skills/hogsend-database/references/client-track-schema.md +97 -0
  31. package/skills/hogsend-database/references/migrations.md +132 -0
  32. package/skills/hogsend-database/references/schema-drift.md +123 -0
  33. package/skills/hogsend-deploy/SKILL.md +62 -0
  34. package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
  35. package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
  36. package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
  37. package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
  38. package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
  39. package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
  40. package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
  41. package/src/bin.ts +73 -111
  42. package/src/commands/contacts.ts +316 -0
  43. package/src/commands/doctor.ts +239 -0
  44. package/src/commands/eject.ts +106 -0
  45. package/src/commands/events.ts +154 -0
  46. package/src/commands/index.ts +36 -0
  47. package/src/commands/journeys.ts +343 -0
  48. package/src/commands/patch.ts +80 -0
  49. package/src/commands/setup.ts +322 -0
  50. package/src/commands/skills.ts +208 -0
  51. package/src/commands/stats.ts +87 -0
  52. package/src/commands/studio.ts +261 -0
  53. package/src/commands/types.ts +41 -0
  54. package/src/commands/upgrade.ts +245 -0
  55. package/src/index.ts +2 -0
  56. package/src/lib/config.ts +147 -0
  57. package/src/lib/http.ts +145 -0
  58. package/src/lib/output.ts +185 -0
  59. package/src/lib/prompt.ts +17 -0
  60. package/src/lib/skills.ts +186 -0
  61. package/studio/assets/index-BVA9GZqq.css +1 -0
  62. package/studio/assets/index-kPwzOOyG.js +230 -0
  63. package/studio/index.html +13 -0
@@ -0,0 +1,142 @@
1
+ # JourneyMeta — trigger, entryLimit, exitOn, suppress
2
+
3
+ `meta` is the static declaration of who enters a journey, how often, and what
4
+ pulls them out. It is the `JourneyMeta` type from `@hogsend/core`:
5
+
6
+ ```ts
7
+ interface JourneyMeta {
8
+ id: string; // stable unique id — used for state + ENABLED_JOURNEYS
9
+ name: string; // human label (becomes user.journeyName)
10
+ description?: string;
11
+ enabled: boolean; // master on/off for this journey
12
+
13
+ trigger: {
14
+ event: string; // the event name that enrolls a user
15
+ where?: PropertyCondition[]; // optional gate on event properties
16
+ };
17
+
18
+ entryLimit: "once" | "once_per_period" | "unlimited";
19
+ entryPeriod?: DurationObject; // required-in-practice for once_per_period
20
+
21
+ exitOn?: Array<{
22
+ event: string;
23
+ where?: PropertyCondition[];
24
+ }>;
25
+
26
+ suppress: DurationObject; // declared cool-down (required field; see note)
27
+ }
28
+ ```
29
+
30
+ ## Fields in detail
31
+
32
+ ### `trigger.event`
33
+
34
+ The event name that enrolls a user. Each journey declares `onEvents:
35
+ [trigger.event]` on its Hatchet durable task, so when `POST /v1/ingest` (or a
36
+ webhook source) pushes that event, Hatchet routes it straight to this journey.
37
+ Use a constant from your `src/journeys/constants/`:
38
+
39
+ ```ts
40
+ trigger: { event: Events.USER_CREATED },
41
+ ```
42
+
43
+ ### `trigger.where` (optional)
44
+
45
+ A `PropertyCondition[]` evaluated against the **enrolling event's properties**.
46
+ If present and not met, the event is skipped with reason
47
+ `trigger_conditions_not_met`. Use it to enroll only a slice of an event:
48
+
49
+ ```ts
50
+ trigger: {
51
+ event: Events.USER_CREATED,
52
+ where: [{ property: "plan", operator: "equals", value: "pro" }],
53
+ },
54
+ ```
55
+
56
+ For the full operator set and how `PropertyCondition` is shaped, see the
57
+ **hogsend-conditions** skill.
58
+
59
+ ### `entryLimit` + `entryPeriod`
60
+
61
+ Controls re-entry:
62
+
63
+ - `"once"` — a user can ever enter this journey exactly one time. A second
64
+ matching event is skipped (`already_entered_once`). Checked against ALL prior
65
+ states for the user+journey, regardless of completion.
66
+ - `"once_per_period"` — re-entry allowed only after `entryPeriod` has elapsed
67
+ since the user's most recent enrollment. Defaults to `hours(24)` if
68
+ `entryPeriod` is omitted, so set it explicitly:
69
+ ```ts
70
+ entryLimit: "once_per_period",
71
+ entryPeriod: days(7),
72
+ ```
73
+ - `"unlimited"` — every matching event enrolls (subject to the active-state
74
+ guard below). Good for test/smoke journeys.
75
+
76
+ ### `exitOn` (optional)
77
+
78
+ Events that pull a user OUT of any in-flight run of this journey. Evaluated by
79
+ the ingestion pipeline whenever a new event arrives for a user with an active
80
+ journey state — if the incoming event name matches an `exitOn` rule (and its
81
+ optional `where` passes), the active run is exited:
82
+
83
+ ```ts
84
+ exitOn: [
85
+ { event: Events.USER_DELETED },
86
+ { event: "subscription.cancelled", where: [{ property: "reason", operator: "equals", value: "churn" }] },
87
+ ],
88
+ ```
89
+
90
+ ### `suppress`
91
+
92
+ A **required** `DurationObject` field declaring an intended cool-down before
93
+ re-entry. Note: the engine's enrollment gates do NOT currently read `suppress` —
94
+ actual re-entry timing is enforced by `entryLimit` + `entryPeriod` (above). It is
95
+ stored as journey metadata and surfaced on the admin journeys API. Treat it as
96
+ the declarative cool-down you pair with `entryLimit` (e.g.
97
+ `entryLimit: "once_per_period"`, `entryPeriod: days(7)`, `suppress: hours(12)`);
98
+ use `hours(0)` on `"unlimited"` test journeys. Because it is required, always set
99
+ it — `hours(0)` when you mean "none".
100
+
101
+ ### `enabled`
102
+
103
+ Master switch. `enabled: false` makes every enrollment skip with reason
104
+ `journey_disabled`. (Admins can ALSO disable a journey at runtime via a
105
+ `journeyConfigs` override — that yields `journey_disabled_by_admin`. Toggle that
106
+ at runtime with the **hogsend-cli** skill's `journeys enable/disable`.)
107
+
108
+ ## The 4-gate enrollment order
109
+
110
+ When the trigger event arrives, the journey task runs these gates IN ORDER
111
+ before `run()` executes. Any failing gate returns `{ status: "skipped", reason }`
112
+ and creates NO state:
113
+
114
+ 1. **`meta.enabled`** (and the admin `journeyConfigs` override) →
115
+ `journey_disabled` / `journey_disabled_by_admin`.
116
+ 2. **`trigger.where`** against the event properties → `trigger_conditions_not_met`.
117
+ 3. **`entryLimit`** (`checkEntryLimit`) → `already_entered_once` /
118
+ `period_not_elapsed`.
119
+ 4. **Email preferences** (`checkEmailPreferences`) — if the user is unsubscribed
120
+ from all → `user_unsubscribed`.
121
+
122
+ After the gates, an **active-state guard** prevents concurrent enrollment: if a
123
+ state in status `active` or `waiting` already exists for this user+journey, the
124
+ event is skipped (`already_active`). This is why a single user never has two
125
+ overlapping runs of the same journey.
126
+
127
+ ## State transitions
128
+
129
+ A `journeyStates` row tracks each run. Once the gates pass:
130
+
131
+ - **enter** → row created with `status: "active"`, `currentNodeId: "start"`.
132
+ - **`ctx.sleep` / `ctx.sleepUntil`** → `status: "waiting"` for the duration, back
133
+ to `"active"` on resume.
134
+ - **`run()` returns** → `status: "completed"`, `completedAt` set, and a
135
+ `journey:completed` event is pushed.
136
+ - **`run()` throws** → `status: "failed"`, `errorMessage` recorded, and a
137
+ `journey:failed` event is pushed; the error re-throws so Hatchet sees the
138
+ failure.
139
+
140
+ Because the gates run before any state is created, a skipped event is invisible
141
+ in `journeyStates` — to debug "why didn't this user enroll?", check the gate
142
+ order above and inspect events with the **hogsend-cli** skill.
@@ -0,0 +1,99 @@
1
+ # Registering a journey
2
+
3
+ Defining a journey is not enough — it has to be in the `journeys` array that the
4
+ client and worker receive. Both processes (HTTP API + Hatchet worker) must see
5
+ the same array: the API needs it to route ingested events, the worker needs it to
6
+ register the durable task that actually runs `run()`.
7
+
8
+ ## 1. Export from `src/journeys/index.ts`
9
+
10
+ Add your journey to the `journeys` array (and re-export it for tests / direct
11
+ reference):
12
+
13
+ ```ts
14
+ import type { DefinedJourney } from "@hogsend/engine";
15
+ import { activation } from "./activation.js";
16
+ import { testOnboarding } from "./test-onboarding.js";
17
+ import { welcome } from "./welcome.js";
18
+
19
+ export const journeys: DefinedJourney[] = [
20
+ welcome,
21
+ testOnboarding,
22
+ activation, // <-- your new journey
23
+ ];
24
+
25
+ export { activation, testOnboarding, welcome };
26
+ ```
27
+
28
+ The exported `journeys` array is the single source of truth that both entry
29
+ points consume.
30
+
31
+ ## 2. It is already threaded into the client + worker
32
+
33
+ In a scaffolded app both entry points import that same array — you usually do
34
+ NOT need to touch these files, just confirm they pass `journeys`:
35
+
36
+ ```ts
37
+ // src/index.ts (HTTP API)
38
+ import { createApp, createHogsendClient } from "@hogsend/engine";
39
+ import { buckets } from "./buckets/index.js";
40
+ import { templates } from "./emails/index.js";
41
+ import { journeys } from "./journeys/index.js";
42
+ import { webhookSources } from "./webhook-sources/index.js";
43
+
44
+ const client = createHogsendClient({ journeys, buckets, email: { templates } });
45
+ const app = createApp(client, { webhookSources });
46
+ ```
47
+
48
+ ```ts
49
+ // src/worker.ts (Hatchet worker — this is what runs run())
50
+ import { createHogsendClient, createWorker } from "@hogsend/engine";
51
+ import { buckets } from "./buckets/index.js";
52
+ import { templates } from "./emails/index.js";
53
+ import { journeys } from "./journeys/index.js";
54
+ import { extraWorkflows } from "./workflows/index.js";
55
+
56
+ const client = createHogsendClient({ journeys, buckets, email: { templates } });
57
+ const worker = createWorker({ container: client, journeys, buckets, extraWorkflows });
58
+ await worker.start();
59
+ ```
60
+
61
+ If you build a new entry point or a test harness, the rule is: **pass the same
62
+ `journeys` array to both `createHogsendClient({ journeys })` and
63
+ `createWorker({ container, journeys })`.** A journey missing from the worker is
64
+ defined but never executes; missing from the client and ingested events won't
65
+ route to it. (`buckets` and `extraWorkflows` are the sibling content arrays the
66
+ scaffold threads through the same way — pass them when present.)
67
+
68
+ ## 3. (Optional) `ENABLED_JOURNEYS`
69
+
70
+ The `ENABLED_JOURNEYS` env var filters which registered journeys actually load —
71
+ comma-separated ids, or `*` (or empty/unset) for all:
72
+
73
+ ```bash
74
+ # only these two run; everything else is registered but inert
75
+ ENABLED_JOURNEYS=welcome,activation
76
+
77
+ # all journeys (default)
78
+ ENABLED_JOURNEYS=*
79
+ ```
80
+
81
+ The filter matches on `meta.id`, so keep ids stable. It is applied identically
82
+ when building the registry (client) and when selecting durable tasks (worker), so
83
+ a journey is either fully on or fully off across both processes. Use it to ship a
84
+ journey's code but keep it dark until you flip the env var — distinct from
85
+ `meta.enabled: false` (code-level off) and the runtime admin toggle (see the
86
+ **hogsend-cli** skill's `journeys enable/disable`).
87
+
88
+ ## Checklist for a new journey
89
+
90
+ 1. New file in `src/journeys/` using `defineJourney({ meta, run })`.
91
+ 2. Any new event / template keys added to `src/journeys/constants/`.
92
+ 3. New email? component + registry entry under `src/emails/` for each
93
+ `Templates.*` key you send.
94
+ 4. Journey added to the `journeys` array in `src/journeys/index.ts`.
95
+ 5. Confirm `src/index.ts` and `src/worker.ts` both receive that array.
96
+ 6. If gating by env, add the id to `ENABLED_JOURNEYS`.
97
+
98
+ To smoke-test it end-to-end against a running instance (enroll a user, watch the
99
+ state reach `completed`), use the **hogsend-cli** skill.
@@ -0,0 +1,82 @@
1
+ # Sending email from a journey
2
+
3
+ `sendEmail()` is a **standalone import from `@hogsend/engine`** — it is NOT a
4
+ method on `ctx`. It renders the template, checks the user's email preferences,
5
+ rewrites links + injects the open pixel (tracking), writes the `email_sends` row,
6
+ and hands off to the provider. You just call it with a template key and props.
7
+
8
+ ```ts
9
+ import { defineJourney, sendEmail } from "@hogsend/engine";
10
+ import { Events, Templates } from "./constants/index.js";
11
+
12
+ export const welcome = defineJourney({
13
+ meta: { /* ... */ },
14
+ run: async (user, ctx) => {
15
+ await sendEmail({
16
+ to: user.email, // recipient
17
+ userId: user.id, // external id (for prefs + tracking)
18
+ journeyStateId: user.stateId, // attributes the send to THIS run
19
+ template: Templates.ACTIVATION_WELCOME, // a key from your emails registry
20
+ subject: "Welcome — let's get you set up",
21
+ journeyName: user.journeyName, // shows on the send record / tags
22
+ props: { firstName: user.properties.firstName }, // template props (optional)
23
+ });
24
+ },
25
+ });
26
+ ```
27
+
28
+ ## The options (`SendEmailOptions`)
29
+
30
+ ```ts
31
+ interface SendEmailOptions {
32
+ to: string; // required — recipient email
33
+ userId: string; // required — external user id
34
+ template: string; // required — registry key (use Templates.*)
35
+ subject: string; // required
36
+ journeyName?: string; // attribution label (defaults to template)
37
+ journeyStateId?: string; // pass user.stateId to tie to this run
38
+ props?: Record<string, unknown>; // props handed to the template component
39
+ }
40
+ ```
41
+
42
+ `name` is auto-derived if you don't pass it: `props.firstName` → `props.name` →
43
+ the local-part of the email → `"there"`. So a template that renders `{name}`
44
+ always has something. Pass `firstName` in `props` for a real name.
45
+
46
+ ## The result (`SendEmailResult`)
47
+
48
+ ```ts
49
+ interface SendEmailResult {
50
+ emailSendId: string; // id of the email_sends row — keep it if you need to correlate
51
+ sentAt: string; // ISO timestamp
52
+ }
53
+
54
+ const { emailSendId, sentAt } = await sendEmail({ /* ... */ });
55
+ ```
56
+
57
+ ## Template keys must exist in your registry
58
+
59
+ `template` is a `Templates.*` constant from your `src/journeys/constants/`. Each
60
+ key must resolve to a real template in `src/emails/` — the constant value (e.g.
61
+ `"activation/welcome"`) is the registry key. If you send a new email, first add:
62
+
63
+ 1. the template component + registry entry under `src/emails/`,
64
+ 2. the matching `Templates` key in `src/journeys/constants/`,
65
+
66
+ then reference it here as `Templates.YOUR_KEY`. A mismatched key fails at render
67
+ time, not compile time, so keep them in lockstep.
68
+
69
+ ## Preferences, tracking, and idempotency
70
+
71
+ - `sendEmail` checks the user's email preferences internally; an unsubscribed
72
+ user is not emailed. You do not need to gate it — though after a long
73
+ `ctx.sleep` it is good practice to branch on `ctx.guard.isSubscribed()` first
74
+ (see `references/branch-on-engagement.md`).
75
+ - Link-click + open tracking is applied automatically by the engine mailer —
76
+ you get it regardless of which provider is configured.
77
+ - `sendEmail` is **not** itself deduped — calling it twice sends twice. Guard
78
+ repeat sends with `ctx.history.email({ email, template })` when a journey can
79
+ re-run a step.
80
+
81
+ For SMS / push / Slack, import the relevant standalone sender — those are also
82
+ plain function imports, never on `ctx`.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: hogsend-cli
3
+ description: Use when an agent needs to inspect or operate a running Hogsend lifecycle engine — querying metrics/contacts/events, listing or enabling/disabling journeys, checking health, or onboarding a local instance — by driving the consolidated `hogsend` CLI. Every data command supports --json for machine-readable output.
4
+ license: MIT
5
+ metadata:
6
+ author: withSeismic
7
+ version: "1.0.0"
8
+ ---
9
+
10
+ # Hogsend CLI
11
+
12
+ The `hogsend` CLI is the agent-native interface to a Hogsend app (the
13
+ code-first lifecycle orchestration engine on PostHog + Resend). It wraps the
14
+ app's `/v1/admin/*` and `/v1/health` HTTP routes, so it works against ANY
15
+ running instance — local (`http://localhost:3002`) or production — without
16
+ importing the database.
17
+
18
+ ## The --json contract (READ THIS FIRST)
19
+
20
+ Every data/read command takes a global `--json` flag. In `--json` mode the
21
+ command prints EXACTLY ONE valid JSON document to stdout and nothing else — no
22
+ spinners, no color, no prose. **Always pass `--json` when you are parsing the
23
+ output programmatically.** Without it, you get a human-pretty table/keyvalue
24
+ rendering meant for a terminal.
25
+
26
+ ```bash
27
+ hogsend stats --json
28
+ hogsend journeys list --json
29
+ hogsend contacts get user_123 --json
30
+ ```
31
+
32
+ On error in `--json` mode the CLI prints `{"error":"<message>"}` to stdout and
33
+ exits 1. On success it exits 0 (doctor is the exception — see below).
34
+
35
+ ## Connecting to an instance
36
+
37
+ Two global flags / env vars control which instance you talk to:
38
+
39
+ - Base URL: `--url <baseUrl>` > `HOGSEND_API_URL` env > `.env` `HOGSEND_API_URL`
40
+ > default `http://localhost:3002`.
41
+ - Admin key: `--admin-key <key>` > `HOGSEND_ADMIN_KEY` env > `ADMIN_API_KEY`
42
+ env > the `.env` equivalents. Sent as `Authorization: Bearer <key>`.
43
+
44
+ ```bash
45
+ hogsend stats --url https://api.example.com --admin-key "$ADMIN_API_KEY" --json
46
+ ```
47
+
48
+ `doctor` hits the unauthenticated `/v1/health` and needs no admin key. Every
49
+ other data command requires one.
50
+
51
+ ## Command map
52
+
53
+ | Command | Purpose |
54
+ |---------|---------|
55
+ | `hogsend doctor` | Health + schema-drift verdict (reachability check). |
56
+ | `hogsend stats` | Overview metrics (contacts, emails, bounce/unsub rates). |
57
+ | `hogsend journeys list/get/enable/disable` | Inspect + toggle journeys. |
58
+ | `hogsend contacts list/get/timeline` | Inspect contacts + their activity. |
59
+ | `hogsend events <userId>` | Raw event stream for one user. |
60
+ | `hogsend skills list/add` | Manage these bundled agent skills. |
61
+ | `hogsend upgrade` | Bump `@hogsend/*` deps to latest + refresh vendored skills. |
62
+ | `hogsend setup` | Interactive LOCAL onboarding (docker, secret, migrate). |
63
+ | `hogsend eject <pkg>` | Vendor a `@hogsend/*` package (unchanged). |
64
+ | `hogsend patch <pkg>` | Wrap `pnpm patch` (unchanged). |
65
+
66
+ Run `hogsend <command> --help` for per-command usage.
67
+
68
+ ## Task playbooks (load the matching reference)
69
+
70
+ - **Query metrics / analyse data** → `references/query-stats.md`
71
+ - **List, inspect, enable or disable journeys** → `references/manage-journeys.md`
72
+ - **Debug why a user did / didn't enroll** → `references/debug-a-journey.md`
73
+ - **Set up a local instance** → `references/setup-local.md`
74
+
75
+ ## Golden rules for agents
76
+
77
+ 1. Pass `--json` whenever you will parse output. Never screen-scrape the table.
78
+ 2. Start a debugging session with `hogsend doctor --json` to confirm the
79
+ instance is reachable and the schema is in sync before trusting other reads.
80
+ 3. Enabling/disabling a journey is a write — confirm intent first.
81
+ 4. Use `--limit`/`--offset` for pagination instead of dumping everything.
@@ -0,0 +1,66 @@
1
+ # Debug a journey: why did (or didn't) a user enroll?
2
+
3
+ A repeatable trace using `doctor` + `journeys get` + `contacts timeline` +
4
+ `events`. Run every command with `--json` and read the output.
5
+
6
+ ## Step 0 — confirm the instance is healthy
7
+
8
+ ```bash
9
+ hogsend doctor --json
10
+ ```
11
+
12
+ Wraps `GET /v1/health`. Inspect the verdict:
13
+
14
+ - `ok` — healthy, proceed.
15
+ - `degraded` — a component (database / redis) is unhealthy; reads may be
16
+ unreliable. Exit code 1.
17
+ - `migration_pending` — the engine schema and the client schema are out of
18
+ sync (`schema.inSync = false`, `pending` migrations listed). Data may be
19
+ missing columns; fix the drift before trusting other queries.
20
+ - `unreachable` — could not connect (HTTP status 0). Check `--url` and that the
21
+ app is running. Exit code 1.
22
+
23
+ Do NOT trust downstream reads until doctor returns `ok`.
24
+
25
+ ## Step 1 — understand the journey's entry rules
26
+
27
+ ```bash
28
+ hogsend journeys get <journeyId> --json
29
+ ```
30
+
31
+ Read the `trigger` (which event enrolls a user) and any `trigger.where`
32
+ property conditions. Read `exitOn` rules (what removes a user). Enrollment is
33
+ gated, in order, by: `enabled` flag → trigger `where` conditions → entry limit
34
+ (once / once_per_period / unlimited) → email preferences (unsubscribed users
35
+ are skipped). Any failed gate means NO enrollment.
36
+
37
+ ## Step 2 — look at the user's lifecycle
38
+
39
+ ```bash
40
+ hogsend contacts get <userId> --json # subscribed? unsubscribed?
41
+ hogsend contacts timeline <userId> --json # merged events/emails/journeys
42
+ ```
43
+
44
+ The timeline shows whether the user already has an active/completed state for
45
+ this journey (entry limit may have blocked re-entry) and whether they're
46
+ unsubscribed (which skips email-sending journeys).
47
+
48
+ ## Step 3 — verify the trigger event actually fired with the right props
49
+
50
+ ```bash
51
+ hogsend events <userId> --event <triggerEvent> --json
52
+ ```
53
+
54
+ Wraps `GET /v1/admin/events?userId=<userId>`. Confirm the trigger event exists
55
+ for this user AND that its properties satisfy the journey's `trigger.where`
56
+ conditions. A missing event, or an event whose properties fail the `where`
57
+ check, is the most common reason a user did not enroll.
58
+
59
+ ## Decision tree
60
+
61
+ - No trigger event in `events` → upstream isn't sending it (PostHog / webhook).
62
+ - Trigger event present but `where` mismatch → property condition not met.
63
+ - Already has a journey state → entry limit blocked re-enrollment.
64
+ - Contact unsubscribed → email journeys skipped at the preferences gate.
65
+ - Journey `enabled: false` (from `journeys get`) → no enrollment at all.
66
+ - `doctor` not `ok` → fix infra/schema first; the data is suspect.
@@ -0,0 +1,53 @@
1
+ # Manage journeys
2
+
3
+ Inspect and toggle lifecycle journeys on a running Hogsend instance. `list` and
4
+ `get` are read-only; `enable`/`disable` are writes.
5
+
6
+ ## List
7
+
8
+ ```bash
9
+ hogsend journeys list --json
10
+ hogsend journeys list --enabled true --limit 50 --offset 0 --json
11
+ ```
12
+
13
+ Wraps `GET /v1/admin/journeys`. Filter with `--enabled <true|false>`, paginate
14
+ with `--limit`/`--offset`. Each row carries `id`, `name`, `enabled`, the
15
+ trigger event, and enrollment counts (active / completed / failed). Use the
16
+ `id` for every other journey command.
17
+
18
+ ## Get detail
19
+
20
+ ```bash
21
+ hogsend journeys get conversion-trial-upgrade --json
22
+ ```
23
+
24
+ Wraps `GET /v1/admin/journeys/{id}`. Returns the full definition view —
25
+ trigger (event + optional `where` conditions), `exitOn` rules, aggregate
26
+ counts, and a sample of recent `journeyStates` (so you can see who is currently
27
+ active / waiting / completed). This is your starting point before toggling a
28
+ journey.
29
+
30
+ ## Enable / disable
31
+
32
+ ```bash
33
+ hogsend journeys enable conversion-trial-upgrade --json
34
+ hogsend journeys disable conversion-trial-upgrade --json
35
+ ```
36
+
37
+ Wraps `PATCH /v1/admin/journeys/{id}` with `{ "enabled": true|false }`.
38
+
39
+ Safety notes:
40
+
41
+ - These are WRITES against a live system. Confirm the journey `id` with
42
+ `journeys get` first, and confirm intent with the human before disabling a
43
+ journey that has active enrollments.
44
+ - Disabling stops NEW enrollments. Contacts already mid-journey continue per
45
+ the engine's semantics — disabling is not a kill switch for in-flight states.
46
+ - After toggling, re-run `journeys get <id> --json` and confirm `enabled`
47
+ flipped as expected.
48
+
49
+ ## Counts cheatsheet
50
+
51
+ When reading counts: `active` = currently enrolled (may be sleeping/waiting),
52
+ `completed` = finished the run, `failed` = errored. A journey with rising
53
+ `failed` counts is worth a `debug-a-journey` pass.
@@ -0,0 +1,66 @@
1
+ # Query metrics, contacts, and events
2
+
3
+ Read-only analysis over a running Hogsend instance. Always pass `--json` when
4
+ parsing.
5
+
6
+ ## Overview metrics
7
+
8
+ ```bash
9
+ hogsend stats --json
10
+ ```
11
+
12
+ Wraps `GET /v1/admin/metrics/overview`. Returns (shape may vary slightly by
13
+ version):
14
+
15
+ ```json
16
+ {
17
+ "totalContacts": 1234,
18
+ "activeJourneys": 5,
19
+ "emailsSent24h": 88,
20
+ "emailsSent7d": 540,
21
+ "emailsSent30d": 2100,
22
+ "bounceRate30d": 0.012,
23
+ "unsubscribeRate": 0.004
24
+ }
25
+ ```
26
+
27
+ Use this for a one-shot snapshot. `bounceRate30d` / `unsubscribeRate` are
28
+ fractions (multiply by 100 for a percentage). A rising bounce rate is the first
29
+ signal of a deliverability problem.
30
+
31
+ ## Contacts
32
+
33
+ ```bash
34
+ # List with search + pagination
35
+ hogsend contacts list --search "@acme.com" --limit 50 --offset 0 --json
36
+
37
+ # A single contact by internal id OR externalId
38
+ hogsend contacts get user_123 --json
39
+
40
+ # Merged activity timeline (events + emails + journeys) for one contact
41
+ hogsend contacts timeline user_123 --json
42
+ ```
43
+
44
+ Wraps `GET /v1/admin/contacts`, `GET /v1/admin/contacts/{id}`, and
45
+ `GET /v1/admin/contacts/{id}/timeline`. `get` includes the contact record plus
46
+ email preferences (subscribed / unsubscribed). The timeline is the fastest way
47
+ to understand a single user's lifecycle history.
48
+
49
+ ## Raw event stream
50
+
51
+ ```bash
52
+ hogsend events user_123 --json
53
+ hogsend events user_123 --event "checkout.completed" --from 2026-01-01T00:00:00Z --to 2026-02-01T00:00:00Z --limit 100 --json
54
+ ```
55
+
56
+ Wraps `GET /v1/admin/events?userId=<userId>`. Filter by `--event`, time window
57
+ (`--from`/`--to`, ISO 8601), and paginate with `--limit`/`--offset`. Use this
58
+ when the timeline isn't granular enough and you need the exact event payloads
59
+ (e.g. to see which properties were present when a journey trigger fired).
60
+
61
+ ## Analysis pattern
62
+
63
+ 1. `hogsend stats --json` for the macro picture.
64
+ 2. `hogsend contacts list --search ... --json` to find the cohort.
65
+ 3. `hogsend contacts timeline <id> --json` to understand individual journeys.
66
+ 4. `hogsend events <id> --event <name> --json` to inspect exact payloads.
@@ -0,0 +1,52 @@
1
+ # Set up a local Hogsend instance
2
+
3
+ `hogsend setup` is interactive LOCAL onboarding — it mirrors the "next steps"
4
+ that `create-hogsend` prints after scaffolding. It is NOT a Railway / cloud
5
+ deploy flow.
6
+
7
+ ## Run it
8
+
9
+ ```bash
10
+ hogsend setup
11
+ ```
12
+
13
+ What it does (interactively, with clack prompts + spinners):
14
+
15
+ 1. `docker compose up -d` — starts TimescaleDB (Postgres), Redis, and
16
+ Hatchet-Lite locally.
17
+ 2. Generates a `BETTER_AUTH_SECRET`.
18
+ 3. Copies `.env.example` to `.env` if `.env` doesn't already exist (it won't
19
+ clobber an existing `.env`).
20
+ 4. Runs `db:migrate` to apply the database schema.
21
+
22
+ Run it from your Hogsend project root (the directory with `docker-compose.yml`
23
+ and `.env.example`).
24
+
25
+ ## Verify
26
+
27
+ After setup, confirm the instance is healthy and the schema is in sync:
28
+
29
+ ```bash
30
+ hogsend doctor --json
31
+ ```
32
+
33
+ Expect a `ok` verdict with `database` and `redis` components healthy and
34
+ `schema.inSync = true`. If you see `migration_pending`, re-run the migration
35
+ step; if `unreachable`, the API isn't running yet — start it with `pnpm dev`
36
+ (default port 3002), then re-run doctor.
37
+
38
+ ## Typical first session
39
+
40
+ ```bash
41
+ hogsend setup # docker up, secret, .env, migrate
42
+ pnpm dev # start the API on :3002 (separate terminal)
43
+ hogsend doctor --json # confirm ok
44
+ hogsend stats --json # sanity-check metrics endpoint
45
+ ```
46
+
47
+ ## Notes
48
+
49
+ - `setup` is interactive by design; in a non-interactive / agent context,
50
+ prefer running the underlying steps explicitly and then `hogsend doctor`.
51
+ - The admin key for local reads comes from your `.env` (`ADMIN_API_KEY` or
52
+ `HOGSEND_ADMIN_KEY`); `doctor` itself needs no key.