@cowliss/cli 0.12.0 → 0.14.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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "checkout_started": {
3
+ "cartId": "cart_10482",
4
+ "checkoutUrl": "https://store.example.com/checkout/10482"
5
+ },
6
+ "purchase_completed": {
7
+ "orderId": "ord_10482"
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,28 @@
1
+ {
2
+ "checkout.started": {
3
+ "cartId": "cart_10482",
4
+ "checkoutUrl": "https://store.example.com/checkout/10482",
5
+ "total": 110.0
6
+ },
7
+ "order.delivered": {
8
+ "orderId": "ord_10482"
9
+ },
10
+ "order.placed": {
11
+ "orderId": "ord_10482",
12
+ "total": 110.0,
13
+ "items": [
14
+ {
15
+ "name": "Linen shirt",
16
+ "price": 78.0
17
+ },
18
+ {
19
+ "name": "Canvas tote",
20
+ "price": 32.0
21
+ }
22
+ ]
23
+ },
24
+ "product.viewed": {
25
+ "productId": "prod_linen_shirt",
26
+ "category": "apparel"
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "level.completed": {
3
+ "level": 4,
4
+ "score": 1250
5
+ },
6
+ "level.failed": {
7
+ "level": 4,
8
+ "attempts": 3
9
+ },
10
+ "purchase.completed": {
11
+ "packId": "starter_bundle",
12
+ "coins": 500,
13
+ "price": 4.99
14
+ },
15
+ "session.started": {
16
+ "platform": "ios",
17
+ "version": "1.2.0"
18
+ },
19
+ "tutorial.completed": {
20
+ "step": "boss_fight"
21
+ }
22
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "invite.sent": {
3
+ "recipientEmail": "colleague@example.com",
4
+ "role": "editor"
5
+ },
6
+ "project.created": {
7
+ "projectId": "proj_123",
8
+ "template": "starter"
9
+ },
10
+ "subscription.created": {
11
+ "plan": "pro",
12
+ "seats": 5
13
+ },
14
+ "trial.started": {
15
+ "plan": "trial",
16
+ "days": 14
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "purchase_completed": {
3
+ "orderId": "ord_10482",
4
+ "amount": 49.99
5
+ }
6
+ }
@@ -1,4 +1,4 @@
1
- import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
  import { CapabilityError } from "./journeys.js";
3
3
  import { z } from "zod";
4
4
 
@@ -1,6 +1,6 @@
1
- import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
  import { CapabilityError } from "./journeys.js";
3
- import { n as readManifest, t as asTemplate } from "./driver-B_pjmy5g.js";
3
+ import { n as readManifest, t as asTemplate } from "./driver-BtQYaFXP.js";
4
4
  import { createElement } from "react";
5
5
  import { renderToStaticMarkup } from "react-dom/server.browser";
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { c as TemplateRenderOutput, o as JourneyStepOutput, s as ManifestOutput } from "./index-DADBwPaG.js";
2
- import { n as Template } from "./emails-5GindgMQ.js";
2
+ import { n as Template } from "./emails-DUwxKLGO.js";
3
3
  //#region src/guest/driver.d.ts
4
4
  /**
5
5
  * The in-process driver: JSON in, JSON out (spec: Guest protocol). One
@@ -1,3 +1,3 @@
1
- import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-B_pjmy5g.js";
1
+ import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-BtQYaFXP.js";
2
2
 
3
3
  export { asTemplate, readManifest, runGuest };
@@ -23,7 +23,7 @@ type SendClass = (typeof SEND_CLASSES)[number];
23
23
  type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
24
24
  /**
25
25
  * The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
26
- * into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
26
+ * into JSON Schema in the manifest and into `types.d.ts`), `subject`,
27
27
  * and the optional metadata below it; the sandbox renders `default`.
28
28
  */
29
29
  type Template<Schema extends z.ZodType = z.ZodType> = {
@@ -1,2 +1,2 @@
1
- import { n as Template, r as SendClass, t as Subject } from "./emails-5GindgMQ.js";
1
+ import { n as Template, r as SendClass, t as Subject } from "./emails-DUwxKLGO.js";
2
2
  export { type SendClass, Subject, Template };
@@ -513,7 +513,7 @@ function uniqueKeys(items, ctx, path) {
513
513
  }
514
514
  }
515
515
  /**
516
- * What `cow build` writes to `.cow/build/manifest.json`, and the shape a
516
+ * What `cow build` writes to `.cowliss/build/manifest.json`, and the shape a
517
517
  * push row stores for good, entry by entry, on the versions it creates.
518
518
  *
519
519
  * `protocol` is any positive integer rather than the current constant on
@@ -33,7 +33,7 @@ declare class CapabilityError extends Error {
33
33
  }
34
34
  /**
35
35
  * The repository's templates, keyed by template key. `cow build` writes the
36
- * real interface into `.cow/types.d.ts` and TypeScript merges it into this
36
+ * real interface into `types.d.ts` and TypeScript merges it into this
37
37
  * one, which is what types `api.send.email`. Empty here on purpose: a
38
38
  * repository that has not built yet still compiles, with string keys.
39
39
  */
@@ -1,4 +1,4 @@
1
- import { a as manifestJourneySchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { a as manifestJourneySchema } from "./journeys-v2-Cndh4CAr.js";
2
2
 
3
3
  //#region src/guest/journeys.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { o as manifestSegmentSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { o as manifestSegmentSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
 
3
3
  //#region src/guest/segments.ts
4
4
  const segmentConfigSchema = manifestSegmentSchema.pick({
package/dist/index.js CHANGED
@@ -8,13 +8,13 @@ import { homedir } from "node:os";
8
8
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
9
9
  import { Command, InvalidArgumentError } from "commander";
10
10
  import { execFile, spawn } from "node:child_process";
11
+ import { existsSync, readFileSync } from "node:fs";
11
12
  import { fileURLToPath, pathToFileURL } from "node:url";
12
13
  import { promisify } from "node:util";
13
14
  import { MessagePort } from "node:worker_threads";
14
15
  import { build, formatMessagesSync } from "esbuild";
15
16
  import { create, extract } from "tar";
16
17
  import { parse } from "@babel/parser";
17
- import { existsSync, readFileSync } from "node:fs";
18
18
  import { createServer } from "node:http";
19
19
  import { createInterface } from "node:readline/promises";
20
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -23,6 +23,40 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
23
  //#region \0rolldown/runtime.js
24
24
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
25
25
 
26
+ //#endregion
27
+ //#region ../../packages/shared/src/agent-prompts/index.ts
28
+ function buildEventsPrompt(args) {
29
+ if (!args.sdkPackage?.trim()) throw new Error("sdkPackage is required");
30
+ if (!args.trackMethod?.trim()) throw new Error("trackMethod is required");
31
+ if (!args.referenceUrl?.trim()) throw new Error("referenceUrl is required");
32
+ if (!Array.isArray(args.events) || args.events.length === 0) throw new Error("events list must not be empty");
33
+ const lines = [
34
+ `Send these events from your application using ${args.sdkPackage}:`,
35
+ "",
36
+ `Reference documentation: ${args.referenceUrl}`,
37
+ ""
38
+ ];
39
+ for (const event of args.events) {
40
+ if (!event.name?.trim()) throw new Error("event name is required");
41
+ lines.push(`### Event: \`${event.name}\``);
42
+ if (event.description) lines.push(`${event.description}`);
43
+ lines.push("");
44
+ if (event.payload) {
45
+ lines.push("Example payload:");
46
+ lines.push("```json");
47
+ lines.push(JSON.stringify(event.payload, null, 2));
48
+ lines.push("```");
49
+ lines.push("");
50
+ }
51
+ lines.push("Code example:");
52
+ lines.push("```ts");
53
+ lines.push(`await ${args.trackMethod}({`, " identifiers: { /* the same identifiers you pass to identify */ },", ` event: ${JSON.stringify(event.name)},`, ` properties: ${JSON.stringify(event.payload ?? {})},`, "});");
54
+ lines.push("```");
55
+ lines.push("");
56
+ }
57
+ return lines.join("\n");
58
+ }
59
+
26
60
  //#endregion
27
61
  //#region ../../packages/shared/src/constants.ts
28
62
  /** The public docs site, for the mail and the pages that point people at it. */
@@ -99,6 +133,12 @@ const IDENTIFIER_KINDS = [
99
133
  "email"
100
134
  ];
101
135
  /**
136
+ * The monthly free allowance, in micro-dollars. Every org gets it, no plan
137
+ * required: it resets at each UTC calendar month and does not roll over.
138
+ * Spend past it draws the prepaid wallet balance.
139
+ */
140
+ const FREE_ALLOWANCE_MICROS = 4e6;
141
+ /**
102
142
  * The fixed-amount one-time top-ups, in micro-dollars ($10 / $50 / $200,
103
143
  * provisional). A topup is a single Stripe Checkout payment; the wallet
104
144
  * credit carries over month to month. Provisional numbers live here so a
@@ -109,6 +149,8 @@ const TOPUP_PRESETS_MICROS = [
109
149
  5e7,
110
150
  2e8
111
151
  ];
152
+ /** Micro-dollars in one dollar; the only place a display conversion starts. */
153
+ const MICRO_DOLLARS_PER_DOLLAR = 1e6;
112
154
  /**
113
155
  * The marketing purpose by name, since it is the one every gate, the
114
156
  * unsubscribe route, and the developer's own toggle all reach for.
@@ -5173,7 +5215,7 @@ function uniqueKeys(items, ctx, path) {
5173
5215
  }
5174
5216
  }
5175
5217
  /**
5176
- * What `cow build` writes to `.cow/build/manifest.json`, and the shape a
5218
+ * What `cow build` writes to `.cowliss/build/manifest.json`, and the shape a
5177
5219
  * push row stores for good, entry by entry, on the versions it creates.
5178
5220
  *
5179
5221
  * `protocol` is any positive integer rather than the current constant on
@@ -5687,6 +5729,10 @@ const walletTopupSchema = z.object({
5687
5729
  completedAt: z.iso.datetime().nullable()
5688
5730
  });
5689
5731
  const listWalletTopupsQuerySchema = paginationQuerySchema;
5732
+ /** Micro-dollars as a plain dollar string, for the whole-dollar constants. */
5733
+ function dollars(micros) {
5734
+ return `$${micros / MICRO_DOLLARS_PER_DOLLAR}`;
5735
+ }
5690
5736
 
5691
5737
  //#endregion
5692
5738
  //#region ../../packages/shared/src/governance.ts
@@ -7673,7 +7719,10 @@ const templateSchema = z.object({
7673
7719
  * manifest entry, and searching inside that is a query the database layer
7674
7720
  * would have to grow a helper for. The badges in each row still show them.
7675
7721
  */
7676
- const listTemplatesQuerySchema = paginationQuerySchema.extend({ q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0) });
7722
+ const listTemplatesQuerySchema = paginationQuerySchema.extend({
7723
+ appId: appIdSchema.optional(),
7724
+ q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0)
7725
+ });
7677
7726
  /** The props a preview or a test send renders the template with. */
7678
7727
  const renderTemplateBodySchema = z.object({ data: z.object({ props: z.record(z.string(), z.unknown()).default({}) }) });
7679
7728
  /** A test send is a delivery like any other, so it takes the same props. */
@@ -8518,8 +8567,8 @@ function isNode(value) {
8518
8567
  */
8519
8568
  const execFileAsync$1 = promisify(execFile);
8520
8569
  /** Where build output lives, relative to the project root. */
8521
- const BUILD_DIR = join(".cow", "build");
8522
- const TYPES_FILE = join(".cow", "types.d.ts");
8570
+ const BUILD_DIR = "build";
8571
+ const TYPES_FILE = "types.d.ts";
8523
8572
  /**
8524
8573
  * The config files the source tarball carries besides `journeys/` and
8525
8574
  * `emails/`. Always the committed `cow.json`, never a `--config` override: a
@@ -8640,7 +8689,7 @@ function assertSegmentResolves(relPath, trigger, segmentKeys) {
8640
8689
  throw new Error(`${relPath}: this journey enrolls from the segment "${trigger.segment}", and there is no segments/${trigger.segment}.ts. ${available} Add that file, or write the predicate list in the trigger.`);
8641
8690
  }
8642
8691
  /**
8643
- * `.cow/types.d.ts`: one `typeof import(...)` per template, merged into the
8692
+ * `types.d.ts`: one `typeof import(...)` per template, merged into the
8644
8693
  * SDK's `CowTemplates`, which is what types `api.send.email`. Written before
8645
8694
  * the typecheck, because the typecheck is what it exists for.
8646
8695
  */
@@ -8650,7 +8699,7 @@ import type { z } from "zod";
8650
8699
 
8651
8700
  declare module "@cowliss/cli/journeys" {
8652
8701
  interface CowTemplates {
8653
- ${templates.map((template) => ` "${template.key}": z.infer<typeof import("../${template.relPath.replace(/\.tsx$/, "")}").props>;`).join("\n")}
8702
+ ${templates.map((template) => ` "${template.key}": z.infer<typeof import("./${template.relPath.replace(/\.tsx$/, "")}").props>;`).join("\n")}
8654
8703
  }
8655
8704
  }
8656
8705
 
@@ -8894,6 +8943,21 @@ function setConfigFile(name) {
8894
8943
  function projectConfigFile() {
8895
8944
  return configFile;
8896
8945
  }
8946
+ /**
8947
+ * The directory a cow project lives in: `.cowliss/` under the repo root, so
8948
+ * a host repository gets one directory instead of four plus a config file.
8949
+ * Standing inside the project directory itself (a `cow.json` right here) is
8950
+ * the other answer, which is how the project directory is also a directory
8951
+ * you can cd into and run `cow` from.
8952
+ *
8953
+ * A missing `.cowliss/` still resolves to it rather than to the cwd: the
8954
+ * error then names the path a project would be at, and `cow init` and
8955
+ * `cow pull` create it.
8956
+ */
8957
+ const PROJECT_DIR = ".cowliss";
8958
+ function resolveProjectDir(cwd = process.cwd()) {
8959
+ return existsSync(join(cwd, projectConfigFile())) ? cwd : join(cwd, PROJECT_DIR);
8960
+ }
8897
8961
  /** The project's config; throws unless `projectDir` holds a valid one. */
8898
8962
  async function assertCowConfig(projectDir) {
8899
8963
  const name = projectConfigFile();
@@ -8931,9 +8995,9 @@ async function configuredApp(projectDir) {
8931
8995
  }
8932
8996
  }
8933
8997
  /**
8934
- * Builds the project at `projectDir`. Writes `.cow/types.d.ts` first (the
8998
+ * Builds the project at `projectDir`. Writes `types.d.ts` first (the
8935
8999
  * typecheck needs it), then the bundles and the source tarball, and
8936
- * `.cow/build/manifest.json` last, so a failed build leaves no manifest and
9000
+ * `build/manifest.json` last, so a failed build leaves no manifest and
8937
9001
  * the next `cow test` rebuilds.
8938
9002
  */
8939
9003
  async function buildProject(projectDir) {
@@ -9242,7 +9306,7 @@ async function copyBlueprint(name, dir, force) {
9242
9306
  }
9243
9307
  function registerAdd(program, io) {
9244
9308
  program.command("add").description("add gallery blueprint code to a cow repo").command("blueprint <name>").description("copy a blueprint's journeys, emails, segments, and scenarios into this repo (local: no API call)").option("--force", "overwrite files that already exist").action(async (name, opts) => {
9245
- const dir = process.cwd();
9309
+ const dir = resolveProjectDir();
9246
9310
  await assertCowConfig(dir);
9247
9311
  const files = await copyBlueprint(name, dir, opts.force === true);
9248
9312
  emit({ data: {
@@ -9589,8 +9653,11 @@ const createRoute = (routeConfig) => {
9589
9653
  //#region ../../packages/shared/src/contract/define.ts
9590
9654
  /**
9591
9655
  * `createRoute` with the contract's additions: a required `operationId`
9592
- * (`<tag>.<verb>`, kept as a literal type so handler maps can key on it) and
9593
- * the `surfaces` block.
9656
+ * (`<tag>.<verb>`, kept as a literal type so handler maps can key on it),
9657
+ * `tags` drawn from `TAG_DOCS`, and the `surfaces` block. An optional
9658
+ * `description` is the text the summary cannot carry: it renders under the
9659
+ * operation in the docs, after the summary in the MCP tool, and after the
9660
+ * options in `cow <command> --help`.
9594
9661
  */
9595
9662
  function defineRoute(config) {
9596
9663
  return createRoute(config);
@@ -9731,6 +9798,7 @@ const apps = defineModule(defineRoute({
9731
9798
  operationId: "apps.update",
9732
9799
  tags: ["apps"],
9733
9800
  summary: "Rename or archive an app",
9801
+ description: "Admin only. Archiving is permanent: an archived app cannot be unarchived or modified. Renaming changes the display name only; the app id never changes.",
9734
9802
  security: SESSION_AUTH,
9735
9803
  request: {
9736
9804
  params: params$11,
@@ -9764,9 +9832,7 @@ const apps = defineModule(defineRoute({
9764
9832
  //#region ../../packages/shared/src/contract/artifacts.ts
9765
9833
  /**
9766
9834
  * Raw artifact transfer, the one place in the API where bytes travel outside
9767
- * an envelope. `cow push` HEADs every digest it built and PUTs only the ones
9768
- * the org does not already have, so a rebuild that changes one journey
9769
- * uploads one bundle.
9835
+ * an envelope (the story is the `artifacts` intro in `tags.ts`).
9770
9836
  *
9771
9837
  * Both routes are hidden from the CLI and MCP surfaces: they are plumbing
9772
9838
  * for the composite `cow push`, not something a human or an agent invokes
@@ -9842,6 +9908,7 @@ const billing = defineModule(defineRoute({
9842
9908
  operationId: "billing.topups.create",
9843
9909
  tags: ["billing"],
9844
9910
  summary: "Start a wallet top-up",
9911
+ description: "Admin only. Returns a hosted checkout URL to redirect to. `amountMicros` must be one of the fixed presets.",
9845
9912
  security: SESSION_AUTH,
9846
9913
  request: { body: jsonBody(createTopupBodySchema) },
9847
9914
  responses: {
@@ -9932,6 +9999,7 @@ const catalog = defineModule(defineRoute({
9932
9999
  operationId: "catalog.events.setState",
9933
10000
  tags: ["catalog"],
9934
10001
  summary: "Allow or deny an event name",
10002
+ description: "Denying an event name permanently drops any entries held for it in the review queue.",
9935
10003
  security: SESSION_AUTH,
9936
10004
  request: {
9937
10005
  params: params$10,
@@ -9988,6 +10056,7 @@ const catalog = defineModule(defineRoute({
9988
10056
  operationId: "catalog.traits.update",
9989
10057
  tags: ["catalog"],
9990
10058
  summary: "Update a catalog trait",
10059
+ description: "A trait type is frozen when registered and cannot be changed. Requests that specify a different `type` are rejected.",
9991
10060
  security: SESSION_AUTH,
9992
10061
  request: {
9993
10062
  params: params$10,
@@ -10005,6 +10074,7 @@ const catalog = defineModule(defineRoute({
10005
10074
  operationId: "catalog.traits.setState",
10006
10075
  tags: ["catalog"],
10007
10076
  summary: "Allow or deny a trait name",
10077
+ description: "Denying a trait name permanently drops any entries held for it in the review queue.",
10008
10078
  security: SESSION_AUTH,
10009
10079
  request: {
10010
10080
  params: params$10,
@@ -10021,6 +10091,7 @@ const catalog = defineModule(defineRoute({
10021
10091
  operationId: "catalog.quarantine.promote",
10022
10092
  tags: ["catalog"],
10023
10093
  summary: "Promote a quarantined name into the catalog",
10094
+ description: "Allow-lists the name in the catalog and backfills all held entries. Held events are recorded with their original timestamps, and held trait values apply to profiles in occurrence order.",
10024
10095
  security: SESSION_AUTH,
10025
10096
  request: { body: jsonBody(quarantineActionBodySchema) },
10026
10097
  responses: {
@@ -10035,6 +10106,7 @@ const catalog = defineModule(defineRoute({
10035
10106
  operationId: "catalog.quarantine.deny",
10036
10107
  tags: ["catalog"],
10037
10108
  summary: "Deny a quarantined name",
10109
+ description: "Denies the name in the catalog and permanently drops all entries held for it in the review queue.",
10038
10110
  security: SESSION_AUTH,
10039
10111
  request: { body: jsonBody(quarantineActionBodySchema) },
10040
10112
  responses: {
@@ -10113,6 +10185,7 @@ const deliveries = defineModule(defineRoute({
10113
10185
  operationId: "deliveries.sesFeedback",
10114
10186
  tags: ["deliveries"],
10115
10187
  summary: "SES delivery feedback via SNS (public)",
10188
+ description: "Updates delivery statuses, adds bounced and complained addresses to your suppression list, and revokes consent on complaints. Open tracking events are rejected because open tracking is disabled. Subscription confirmation requests from the platform's email provider are confirmed automatically.",
10116
10189
  security: NO_AUTH,
10117
10190
  surfaces: HIDDEN_FROM_TOOLS,
10118
10191
  request: { body: { content: { "text/plain": { schema: z.string() } } } },
@@ -10126,6 +10199,7 @@ const deliveries = defineModule(defineRoute({
10126
10199
  operationId: "deliveries.unsubscribe",
10127
10200
  tags: ["deliveries"],
10128
10201
  summary: "One-click unsubscribe (public)",
10202
+ description: "Revokes consent for the purpose the email was sent under on the recipient's profile. Setting `all` to `1` revokes the master `marketing` purpose instead, stopping all marketing email.",
10129
10203
  security: NO_AUTH,
10130
10204
  surfaces: HIDDEN_FROM_TOOLS,
10131
10205
  request: { query: tokenQuery.extend({
@@ -10233,6 +10307,7 @@ const domains = defineModule(defineRoute({
10233
10307
  operationId: "domains.update",
10234
10308
  tags: ["domains"],
10235
10309
  summary: "Toggle click tracking",
10310
+ description: "Enabling click tracking requires a verified domain, while disabling is always allowed. Tracking covers every email sent from the domain, including transactional email.",
10236
10311
  security: SESSION_AUTH,
10237
10312
  request: {
10238
10313
  params: params$9,
@@ -10249,6 +10324,7 @@ const domains = defineModule(defineRoute({
10249
10324
  operationId: "domains.delete",
10250
10325
  tags: ["domains"],
10251
10326
  summary: "Give up a sending domain",
10327
+ description: "Sending from this domain stops immediately, and the domain becomes available for any organization to claim.",
10252
10328
  security: SESSION_AUTH,
10253
10329
  request: { params: params$9 },
10254
10330
  responses: {
@@ -10303,6 +10379,7 @@ const emails = defineModule(defineRoute({
10303
10379
  operationId: "emails.verify",
10304
10380
  tags: ["emails"],
10305
10381
  summary: "Verify an email address (public)",
10382
+ description: "Marks the address verified. If the profile associated with the link still carries that address, sets `emailVerified` to `true` and records a `system.email_verified` event.",
10306
10383
  security: NO_AUTH,
10307
10384
  surfaces: HIDDEN_FROM_TOOLS,
10308
10385
  request: { query: tokenQuery },
@@ -10360,6 +10437,7 @@ const events = defineModule(defineRoute({
10360
10437
  operationId: "apps.events.seen",
10361
10438
  tags: ["events"],
10362
10439
  summary: "Whether an app has ever received these event names",
10440
+ description: "A name may be a pattern where `*` matches any run of characters against the whole name. A pattern reports the latest timestamp among matching events, or `null` if the app has never received one.",
10363
10441
  security: PIPELINE_AUTH,
10364
10442
  surfaces: { cli: false },
10365
10443
  request: {
@@ -10421,6 +10499,7 @@ const executions = defineModule(defineRoute({
10421
10499
  operationId: "executions.cancel",
10422
10500
  tags: ["executions"],
10423
10501
  summary: "Cancel live executions of a journey, or of one version",
10502
+ description: "Admin only. Each execution stops when it reaches its next step.",
10424
10503
  security: SESSION_AUTH,
10425
10504
  request: { body: jsonBody(cancelExecutionsBodySchema) },
10426
10505
  responses: {
@@ -10531,6 +10610,7 @@ const ingestion = defineModule(defineRoute({
10531
10610
  operationId: "ingestion.batch",
10532
10611
  tags: ["ingestion"],
10533
10612
  summary: "Import one source's calls in one request",
10613
+ description: "Batch writes skip triggers on raw events, but segments still recompute and a journey triggered by segment entry can fire. A failing item is reported in the response and does not fail the rest of the batch.",
10534
10614
  security: API_KEY_AUTH,
10535
10615
  surfaces: HIDDEN_FROM_TOOLS,
10536
10616
  request: { body: jsonBody(batchBodySchema) },
@@ -10611,6 +10691,7 @@ const journeys = defineModule(defineRoute({
10611
10691
  operationId: "journeys.setStatus",
10612
10692
  tags: ["journeys"],
10613
10693
  summary: "Put journeys on, off or on hold",
10694
+ description: "Admin only. Turning a journey off lets executions in flight finish, while putting it on hold pauses them before their next step. Turning a held journey on resumes them.",
10614
10695
  security: PIPELINE_AUTH,
10615
10696
  surfaces: { cli: false },
10616
10697
  request: { body: jsonBody(setJourneysStatusBodySchema) },
@@ -10641,6 +10722,7 @@ const journeys = defineModule(defineRoute({
10641
10722
  operationId: "journeys.delete",
10642
10723
  tags: ["journeys"],
10643
10724
  summary: "Delete a journey",
10725
+ description: "Admin only. Executions already in flight finish, but no new ones start. A push never deletes a journey, so this is the only way to remove one whose file was deleted from your code.",
10644
10726
  security: SESSION_AUTH,
10645
10727
  request: { params: params$7 },
10646
10728
  responses: {
@@ -10667,6 +10749,7 @@ const journeys = defineModule(defineRoute({
10667
10749
  operationId: "journeys.startEnrollment",
10668
10750
  tags: ["journeys"],
10669
10751
  summary: "Enroll the journey's current members",
10752
+ description: "Admin only. Starts an enrollment for recipients currently in the journey's segment. If an enrollment is already running, this returns the existing enrollment rather than starting another.",
10670
10753
  security: SESSION_AUTH,
10671
10754
  surfaces: HIDDEN_FROM_TOOLS,
10672
10755
  request: { params: params$7 },
@@ -10694,6 +10777,7 @@ const journeys = defineModule(defineRoute({
10694
10777
  operationId: "journeys.cancelEnrollment",
10695
10778
  tags: ["journeys"],
10696
10779
  summary: "Stop a running enrollment",
10780
+ description: "Admin only. Recipients already enrolled remain in the journey and their executions continue. Stopping the enrollment only prevents remaining recipients from enrolling.",
10697
10781
  security: SESSION_AUTH,
10698
10782
  request: { params: params$7 },
10699
10783
  responses: {
@@ -10707,6 +10791,7 @@ const journeys = defineModule(defineRoute({
10707
10791
  operationId: "journeys.dryRun",
10708
10792
  tags: ["journeys"],
10709
10793
  summary: "Dry-run a journey against a real user",
10794
+ description: "Admin only. Runs the journey's latest ready version against the specified profile with sends disabled, delivering no emails or webhooks. The execution proceeds even if the journey is turned off.",
10710
10795
  security: SESSION_AUTH,
10711
10796
  request: {
10712
10797
  params: params$7,
@@ -10801,6 +10886,7 @@ const pushes = defineModule(defineRoute({
10801
10886
  operationId: "pushes.create",
10802
10887
  tags: ["pushes"],
10803
10888
  summary: "Push a built app",
10889
+ description: "Every bundle and the source archive must be uploaded before creating the push. The push is refused if any referenced artifact is missing, or if a journey's `from` address uses a domain your organization has not verified.",
10804
10890
  security: PIPELINE_AUTH,
10805
10891
  surfaces: HIDDEN_FROM_TOOLS,
10806
10892
  request: { body: jsonBody(createPushBodySchema) },
@@ -11041,6 +11127,7 @@ const segments = defineModule(defineRoute({
11041
11127
  operationId: "segments.update",
11042
11128
  tags: ["segments"],
11043
11129
  summary: "Update a segment",
11130
+ description: "Updating a segment's `definition` recomputes its membership over existing profile history. A segment defined in code (in a journey trigger or your project's `segments/` directory) cannot be updated here: change the code and push the project again.",
11044
11131
  security: SESSION_AUTH,
11045
11132
  request: {
11046
11133
  params: params$5,
@@ -11058,6 +11145,7 @@ const segments = defineModule(defineRoute({
11058
11145
  operationId: "segments.delete",
11059
11146
  tags: ["segments"],
11060
11147
  summary: "Delete a segment",
11148
+ description: "Deleting a segment permanently removes all of its members. A segment defined in code (in a journey trigger or your project's `segments/` directory) cannot be deleted here: remove it from your project and push again.",
11061
11149
  security: SESSION_AUTH,
11062
11150
  request: { params: params$5 },
11063
11151
  responses: {
@@ -11102,6 +11190,7 @@ const settings = defineModule(defineRoute({
11102
11190
  operationId: "settings.update",
11103
11191
  tags: ["settings"],
11104
11192
  summary: "Update org settings",
11193
+ description: "Admin only. Lowering `eventRetentionDays` permanently deletes stored events older than the new window.",
11105
11194
  security: SESSION_AUTH,
11106
11195
  request: { body: jsonBody(updateOrgSettingsBodySchema) },
11107
11196
  responses: {
@@ -11167,6 +11256,7 @@ const settings = defineModule(defineRoute({
11167
11256
  operationId: "settings.pipelineKeys.revoke",
11168
11257
  tags: ["settings"],
11169
11258
  summary: "Revoke an org pipeline key",
11259
+ description: "Revocation is permanent. Calls authenticating with this key can still be accepted for up to a minute.",
11170
11260
  security: SESSION_AUTH,
11171
11261
  request: {
11172
11262
  params: z.object({ id: z.string() }),
@@ -11183,6 +11273,7 @@ const settings = defineModule(defineRoute({
11183
11273
  operationId: "settings.apiKeys.revoke",
11184
11274
  tags: ["settings"],
11185
11275
  summary: "Revoke an org API key",
11276
+ description: "Revocation is permanent. Calls authenticating with this key stop being accepted immediately.",
11186
11277
  security: SESSION_AUTH,
11187
11278
  request: {
11188
11279
  params: z.object({ id: z.string() }),
@@ -11200,10 +11291,9 @@ const settings = defineModule(defineRoute({
11200
11291
  const appParams = z.object({ appId: z.string() });
11201
11292
  const params$4 = z.object({ id: z.string() });
11202
11293
  /**
11203
- * A source is one inbound pipe into an app. It is created and listed under
11204
- * its parent app (the app is the attribution unit) and addressed by its own
11205
- * id afterwards. The webhook receiver is public: the provider's signature is
11206
- * its authorization.
11294
+ * Sources: created and listed under their parent app (so those two routes
11295
+ * carry the `apps` tag), addressed by their own id afterwards. The customer
11296
+ * story is the `sources` intro in `tags.ts`.
11207
11297
  */
11208
11298
  const sources = defineModule(defineRoute({
11209
11299
  method: "post",
@@ -11272,6 +11362,7 @@ const sources = defineModule(defineRoute({
11272
11362
  operationId: "sources.archive",
11273
11363
  tags: ["sources"],
11274
11364
  summary: "Archive a source",
11365
+ description: "Admin only. Archiving is irreversible and immediately stops this source from accepting calls or webhook deliveries. For an `api` source, this halts ingestion for the parent app until you add a replacement.",
11275
11366
  security: SESSION_AUTH,
11276
11367
  request: { params: params$4 },
11277
11368
  responses: {
@@ -11320,6 +11411,7 @@ const suppressions = defineModule(defineRoute({
11320
11411
  operationId: "suppressions.delete",
11321
11412
  tags: ["suppressions"],
11322
11413
  summary: "Un-suppress an address",
11414
+ description: "Admin only. Clears the address at the email provider so future sends can be attempted. If the provider cannot be reached, the address remains suppressed.",
11323
11415
  security: SESSION_AUTH,
11324
11416
  request: { params: z.object({ id: z.string() }) },
11325
11417
  responses: {
@@ -11330,19 +11422,160 @@ const suppressions = defineModule(defineRoute({
11330
11422
  }));
11331
11423
 
11332
11424
  //#endregion
11333
- //#region ../../packages/shared/src/contract/templates.ts
11425
+ //#region ../../packages/shared/src/contract/tags.ts
11334
11426
  /**
11335
- * Templates: the emails a journey sends, addressed by key like a journey.
11336
- * A template has versions and nothing else (ADR 0011), so there is no
11337
- * create and no update here: templates arrive with a push, and the only
11338
- * writes are rendering one to look at it, sending one to yourself, and
11339
- * deleting the key.
11427
+ * The prose for every contract tag: the API reference page's title,
11428
+ * description and intro, the OpenAPI document's `tags[]`, and the CLI's help
11429
+ * line for a command group named after a tag. Route `tags` are typed against
11430
+ * this record, so a new tag without an entry fails typecheck.
11340
11431
  *
11341
- * `preview` and `testSend` both render the latest version that finished
11342
- * compiling, in the sandbox, with the props the caller passes. `testSend`
11343
- * then sends that message to the caller's own address through the
11344
- * organization's default sender: it reaches no recipient, so it passes no
11345
- * consent gate, counts against nothing, and is logged as a test.
11432
+ * Intro links are docs-site-relative (`/tracking/apps/`): the docs render
11433
+ * them as written, and `buildOpenApiDocument` prefixes them with DOCS_URL.
11434
+ */
11435
+ const TAG_DOCS = {
11436
+ health: {
11437
+ title: "Health",
11438
+ description: "Liveness and readiness probes.",
11439
+ intro: "These routes are orchestration probes. Both sit outside the `/v1` prefix and need no credential."
11440
+ },
11441
+ ingestion: {
11442
+ title: "Ingestion",
11443
+ description: "POST /v1/identify, /v1/track, and /v1/batch: the org API-key-authenticated write path.",
11444
+ intro: "The write path ingests traits and events using an organization API key (`Authorization: Bearer <key>`). All calls sit behind the organization rate limiter and the billing hard stop, and dedupe retries using `Idempotency-Key` or `messageId`. Every call requires the `sourceId` of an `api` source, from which Cowliss derives and stamps the app (see [Apps tracking](/tracking/apps/)). Calls with an unknown, archived, or non-API `sourceId` are rejected. Event names starting with `system.` are rejected because that prefix is reserved for events Cowliss writes itself (see [Event patterns](/journeys/#the-system-namespace))."
11445
+ },
11446
+ users: {
11447
+ title: "Users",
11448
+ description: "Profiles: list, search, the 360 view, consent editing, merged event history, segment membership, GDPR export and erasure.",
11449
+ intro: "A profile holds what an app knows about a recipient: traits, identifiers, the consent map, merged event history, and segment membership. When a profile is merged into another, requests to its `usr_` id redirect to the survivor's URL. GDPR export and erasure can target a single profile by its `usr_` id, taking everything merged into it, or every profile across your apps that carries an identifier. All routes require session-token authentication, and erasure requires `org:admin`."
11450
+ },
11451
+ consent: {
11452
+ title: "Consent purposes",
11453
+ description: "The consent purposes your organization's profiles answer: `marketing` plus the ones your own code declares.",
11454
+ intro: "Every profile's `consent` map answers these purposes, and every switch that offers one is labelled from here. One is fixed for every organization, `marketing`, the master switch every other purpose sits under; the rest are whatever your code declares under `purposes` in [`cow.json`](/guides/repo/). `transactional` is not here: it is why a receipt or an alert goes out, which is not something a recipient chooses, so nothing stores an answer to it. Purposes are code, like journeys and templates, so this resource is read-only: a push is what adds or relabels one, and nothing ever deletes one, because profiles already hold answers against it."
11455
+ },
11456
+ events: {
11457
+ title: "Events",
11458
+ description: "The org-wide event feed, per-app volume stats, and single-event fetch.",
11459
+ intro: "These routes provide read views over your event history. Events are append-only and never rewritten. Ingestion writes are on the [Ingestion](/reference/api/ingestion/) page."
11460
+ },
11461
+ apps: {
11462
+ title: "Apps",
11463
+ description: "The app registry: your apps and products, and the sources that push into them.",
11464
+ intro: "An app is the attribution unit for one product: it carries no kind, configuration, or credential of its own, and each event and profile carries its `appId`. Inbound pipes into an app are sources created under it, and all of an app's sources land on the same profiles. This includes the first-party `api` source created with the app, which the SDK and HTTP path write through. Routes that address a source by its own id (including the public webhook receiver) are on the [Sources](/reference/api/sources/) page. See the [Apps and sources guide](/tracking/apps/)."
11465
+ },
11466
+ sources: {
11467
+ title: "Sources",
11468
+ description: "Sources addressed by id: config rotation, archiving, and the public webhook receiver.",
11469
+ intro: "A source is one inbound pipe into an [app](/reference/api/apps/): each app is created with the first-party `api` source, and a `kind` is singleton per app. Sources are created and listed under their parent app, and these routes address one by its own `id`. All writes are attributed to the source's parent app. The signing secret in `config` is write-only and never present in any response: `configured` reports whether one is stored. The webhook receiver is public: the caller's signature is the authorization, and the route answers with plain text rather than the envelope. See the [Apps and sources guide](/tracking/apps/)."
11470
+ },
11471
+ catalog: {
11472
+ title: "Catalog",
11473
+ description: "The tracking plan: event and trait definitions, governance states (allow/deny), and the strict-mode quarantine review queue.",
11474
+ intro: "The catalog is the tracking plan: event and trait definitions addressed by name, unique per organization, each in a governance state (`allowed`, `denied`, or `quarantined`, where denied always wins). Ingestion accepts by default, rejecting only denied names and canonical traits that fail format validation. With the org's `ingestionPolicy` set to `strict`, unclassified names are held in the quarantine review queue instead of applied. Property and trait types: " + propertyTypeSchema.options.map((type) => `\`${type}\``).join(", ") + ". Observation infers an array's item type when its members agree, and `array<any>` when they do not or the array is empty."
11475
+ },
11476
+ review: {
11477
+ title: "Review queue",
11478
+ description: "One list over both halves of the review queue: names held unapplied under a strict policy, and names accepted but flagged under a permissive one.",
11479
+ intro: "The review queue brings together everything ingestion parked for a person to decide on, replacing the two lists it unions. A held name is a write a strict policy would not apply until its name is classified; a violation is a name a permissive policy accepted and flagged. Both carry a `kind` and `name` alongside a first and a last sighting, so they list together under one cursor, ordered by last seen. This resource is a read and nothing else, as the per-item verbs stay where they are: allow or deny a held name on the [Catalog](/reference/api/catalog/) page, and resolve or dismiss a violation on the [Violations](/reference/api/violations/) page."
11480
+ },
11481
+ violations: {
11482
+ title: "Violations",
11483
+ description: "Tracking-plan violations: unknown names accepted but flagged, with one-click resolve into the catalog or dismissal.",
11484
+ intro: "In permissive mode, an unknown event or trait name is accepted and flagged here, so a typo never silently drops data. Resolving a violation registers the name into [the catalog](/reference/api/catalog/) pre-filled from the observed payload, while dismissing silences noise. Resolving and dismissing are admin only."
11485
+ },
11486
+ segments: {
11487
+ title: "Segments",
11488
+ description: "Computed audiences: predicate definitions, preview, member lists, and the recomputation lifecycle.",
11489
+ intro: "Segments are predicate definitions over traits and event history, recomputed synchronously on each write. Transitions emit `system.segment_entered` and `system.segment_exited` system events that journeys trigger on, except those found by a bulk recompute, which start nobody. See the [Segments guide](/segments/) for predicate semantics. All mutations require organization admin permissions."
11490
+ },
11491
+ journeys: {
11492
+ title: "Journeys",
11493
+ description: "One journey per key: trigger, tags, recorded step spine, the enable switch, and operational stats.",
11494
+ intro: "A journey is addressed by the key its author gave the file, running its latest ready version. Status decides whether the journey fires, and a push never touches it: a key is off until it is turned on. The `spine` is read from the journey's source and is display only: what a journey does during execution is whatever its code decides. A journey belongs to one app and fires on that app's events only. A trigger's `event` is a pattern in which `*` matches any sequence of characters, one or a list of them, and a pattern reaches Cowliss's own `system.` events only when its literal prefix starts with `system.`: see [Event patterns](/journeys/#event-patterns). Executions are on the [Executions](/reference/api/executions/) page."
11495
+ },
11496
+ templates: {
11497
+ title: "Templates",
11498
+ description: "The email templates a journey sends: their props, who sends them, rendering one, and sending yourself a test.",
11499
+ intro: "Templates are the email templates a journey sends, each addressed by the key its author gave the file. Templates arrive with a push, so there is no create and no update here. A template has versions and nothing else, no switch, because a send resolves the latest version that finished compiling at the moment it sends, so fixing copy reaches executions already in flight."
11500
+ },
11501
+ artifacts: {
11502
+ title: "Artifacts",
11503
+ description: "Content-addressed bundle and source storage: HEAD to check, PUT to upload.",
11504
+ intro: "Artifacts are immutable bytes named by their own `sha256:` digest and deduplicated within an organization. They serve as plumbing for `cow push` so changing one journey transfers only one bundle, and remain documented for anyone reimplementing push. This is the one place in the API where bytes travel outside an envelope. The body of a `PUT` must hash to the digest it is filed under, and repeat transfers of the same digest are a no-op. These routes carry their own size limit rather than the global request one."
11505
+ },
11506
+ pushes: {
11507
+ title: "Pushes",
11508
+ description: "One push per cow push: the whole project at one point in time, and the versions it compiles.",
11509
+ intro: "A push stores the app's source archive and creates a new version of every journey and template whose bundle changed; a key that did not change gets none. The push returns as soon as it is stored, and each version compiles on its own, so one file that cannot compile costs its own key and nothing else. A push turns nothing on: journey `status` is untouched."
11510
+ },
11511
+ versions: {
11512
+ title: "Versions",
11513
+ description: "One key's history: every version of a journey or a template, newest first.",
11514
+ intro: "A version is an immutable, content-addressed snapshot of one journey or one template. It is read-only and addressed through its key rather than on its own path. Journeys and templates share a key space, so both `key` and `kind` are required. The latest `ready` version is the code new executions and new sends use, which leaves the previous version running when a compile fails."
11515
+ },
11516
+ executions: {
11517
+ title: "Executions",
11518
+ description: "One run of one journey for one profile, pinned to the version it started on.",
11519
+ intro: "An execution is one journey for one profile, pinned to the version it started on. It can be filtered by journey, version, status, and profile in one place. Its detail carries what the journey logged with `api.log`. Pipeline keys can read executions, but stopping them is explicit: `cancel` is the only write and requires a dashboard session."
11520
+ },
11521
+ webhooks: {
11522
+ title: "Webhooks",
11523
+ description: "The URLs a journey POSTs a Standard-Webhooks-signed payload to, one registry the journey references by name.",
11524
+ intro: "Journeys reference webhooks by name, so journey code never hardcodes a URL. A webhook carries its own signing secret, shown only at creation. Payloads follow the Standard Webhooks convention with the `v: 1` version field and `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. A webhook with too many consecutive failures auto-disables until re-enabled. All mutations require `org:admin`."
11525
+ },
11526
+ domains: {
11527
+ title: "Sending domains",
11528
+ description: "Org-verified sending domains: claims, the DNS records to install, verification status, and manual re-check.",
11529
+ intro: "An organization cannot send without a verified domain. Claims are global and exact-string: a parent or child of a claimed domain conflicts. The `dnsRecords` array specifies what your DNS must carry (three DKIM CNAMEs, the MX return path and its SPF TXT, plus a recommended DMARC TXT), each with its own status. DNS is re-checked on a schedule and nothing pushes an event when it is, so status refreshes are pull-only."
11530
+ },
11531
+ deliveries: {
11532
+ title: "Deliveries",
11533
+ description: "The global delivery log (every email and webhook attempted, with status progression) plus the public SES feedback and one-click unsubscribe endpoints.",
11534
+ intro: "Each send attempt creates a delivery. A delivery is `sent` then `delivered`, `bounced` or `complained`, or it is `failed`, a typed `skipped_*`, or a dry-run `would_*`. Status precedence ensures that a late `delivered` status never overwrites `bounced`, and a complaint outranks a bounce. For webhooks, a successful response confirms delivery, making `delivered` terminal. Public routes for delivery feedback and RFC 8058 one-click unsubscribe verify signatures instead of sessions, answering per the caller's convention rather than the standard envelope. Address verification is on the [emails](/reference/api/emails/) page."
11535
+ },
11536
+ emails: {
11537
+ title: "Emails",
11538
+ description: "The public email-address verification pages: the signed link in a verification email, answered as HTML.",
11539
+ intro: "These routes handle direct transactional email sending and public email-address verification. The `emails.send` route is the first-party shape of the same send the [Resend facade](/resend/) serves, authenticating with an org API key and answering in the envelope with the delivery id. The public verification routes require no Cowliss credentials and answer with HTML pages rather than the envelope. Verification links authorize through a single-use `?token=` that expires after 48 hours, and an invalid, expired, or already-spent token is rejected. The `GET` request only offers the verification and the `POST` request performs it, so a link scanner that fetches the URL does not verify the address."
11540
+ },
11541
+ suppressions: {
11542
+ title: "Suppressions",
11543
+ description: "Cowliss's mirror of your organization's suppression list: the cheap send-gate check and the dashboard's why.",
11544
+ intro: "Cowliss mirrors the suppression list your organization accumulates from hard bounces and complaints per address so the send gate skips cheaply and the dashboard can say why. Suppression is a separate fact from [consent](/reference/api/consent/): a complaint revokes consent, while a bounce only suppresses the address. The list is your organization's own: an address one organization suppresses is untouched for every other."
11545
+ },
11546
+ settings: {
11547
+ title: "Settings",
11548
+ description: "Org settings: ingestion policy, event retention, the sending-pause state, and self-service org API keys and pipeline keys.",
11549
+ intro: "Organization settings configure data handling: `ingestionPolicy` decides whether unclassified event and trait names are accepted and flagged or quarantined for review, `eventRetentionDays` keeps events for at most 18 months, and `sendingPaused` is a platform pause that the organization can read but only the platform sets. This module is also the self-service home of the organization's two key families: ingestion API keys, which write events and reach nothing else, and pipeline keys, which push an app and reach nothing else. Any member can list either family, but only admins create and revoke them. Neither list shows the other family's keys, and revoking a key from the other family cannot find it. See the [Keys guide](/guides/keys/)."
11550
+ },
11551
+ billing: {
11552
+ title: "Billing",
11553
+ description: "The prepaid wallet: metered spend against the monthly free allowance, fixed-amount Stripe top-ups, and the hard stop at zero credit.",
11554
+ intro: "Spend is one weighted total over events, emails, webhooks and journey runs, counted from what already happened, never billed in arrears. Credit is the monthly included usage (" + dollars(FREE_ALLOWANCE_MICROS) + ", resets each UTC month, no rollover) plus the prepaid wallet (carries over). At zero credit, the platform hard-stops: ingestion is refused and sends are skipped. See the [Billing guide](/guides/billing/)."
11555
+ },
11556
+ openapi: {
11557
+ title: "OpenAPI",
11558
+ description: "The OpenAPI 3.1 document, built from this contract.",
11559
+ intro: "The document is built from the route contract. Requests to `GET /openapi.json` are served unauthenticated."
11560
+ },
11561
+ me: {
11562
+ title: "Me",
11563
+ description: "The signed-in developer's own settings.",
11564
+ intro: "These routes manage the signed-in developer's own settings. Like the rest of the dashboard API, requests use session authentication, but this resource is deliberately not org-scoped: the subject is the caller's own platform-workspace profile, taken from the session, never from the request. They are hidden from the CLI and the MCP server because both act with an organization key on that organization's data without a signed-in human. An operator changing an end user's consent uses `users.updateConsent` instead, which is the same decision made by someone else."
11565
+ },
11566
+ resend: {
11567
+ title: "Resend-compatible send",
11568
+ description: "The Resend-compatible send facade: point `RESEND_BASE_URL` at `/resend/{appOrSourceId}` and keep your code.",
11569
+ intro: "The Resend-compatible send facade lets developers point `RESEND_BASE_URL` at `/resend/{appOrSourceId}` and keep their code. The `appOrSourceId` path segment accepts an app id or the id of its API source, because the Resend SDK carries no other field and accepts no custom header. The bearer is the organization's ingestion key. Every send creates a delivery and requires a from-address on a domain the organization has verified. These routes answer Resend's own shapes rather than the envelope."
11570
+ }
11571
+ };
11572
+
11573
+ //#endregion
11574
+ //#region ../../packages/shared/src/contract/templates.ts
11575
+ /**
11576
+ * Templates, addressed by key like a journey. No create and no update: a
11577
+ * template has versions and nothing else (ADR 0011). What `preview` and
11578
+ * `testSend` do is the `templates` intro in `tags.ts`.
11346
11579
  */
11347
11580
  const params$3 = z.object({ key: z.string() });
11348
11581
  const templates = defineModule(defineRoute({
@@ -11393,6 +11626,7 @@ const templates = defineModule(defineRoute({
11393
11626
  operationId: "templates.testSend",
11394
11627
  tags: ["templates"],
11395
11628
  summary: "Send a rendered template to your own address",
11629
+ description: "Sends through your organization's default sender. The test goes to you regardless of your consent settings, costs nothing, and shows as a test.",
11396
11630
  security: SESSION_AUTH,
11397
11631
  request: {
11398
11632
  params: params$3,
@@ -11409,6 +11643,7 @@ const templates = defineModule(defineRoute({
11409
11643
  operationId: "templates.delete",
11410
11644
  tags: ["templates"],
11411
11645
  summary: "Delete an email template",
11646
+ description: "Admin only. Deleting a key deletes every version of it and is refused while a journey still sends it, naming the journeys.",
11412
11647
  security: SESSION_AUTH,
11413
11648
  request: { params: params$3 },
11414
11649
  responses: {
@@ -11489,6 +11724,7 @@ const users = defineModule(defineRoute({
11489
11724
  operationId: "users.eraseByIdentifier",
11490
11725
  tags: ["users"],
11491
11726
  summary: "Erase every person one identifier names (GDPR)",
11727
+ description: "Admin only. Terminates active journey executions, then permanently deletes every profile across your apps that carries the identifier, along with their merged profiles, and their event and delivery history. This action cannot be undone.",
11492
11728
  security: SESSION_AUTH,
11493
11729
  request: { body: jsonBody(eraseUserByIdentifierBodySchema) },
11494
11730
  responses: {
@@ -11516,6 +11752,7 @@ const users = defineModule(defineRoute({
11516
11752
  operationId: "users.erase",
11517
11753
  tags: ["users"],
11518
11754
  summary: "Erase a user (GDPR)",
11755
+ description: "Admin only. Terminates active journey executions, then permanently deletes the profile, any profiles merged into it, and their event and delivery history. This action cannot be undone.",
11519
11756
  security: SESSION_AUTH,
11520
11757
  request: { params: params$2 },
11521
11758
  responses: {
@@ -11636,6 +11873,7 @@ const violations = defineModule(defineRoute({
11636
11873
  operationId: "violations.resolve",
11637
11874
  tags: ["violations"],
11638
11875
  summary: "Resolve a violation into the catalog",
11876
+ description: "Admin only. Resolving is irreversible and refuses violations that have already been handled or whose name is already in the catalog. You must supply `data.type` when resolving a trait that was only observed as `null`.",
11639
11877
  security: SESSION_AUTH,
11640
11878
  request: {
11641
11879
  params: params$1,
@@ -11652,6 +11890,7 @@ const violations = defineModule(defineRoute({
11652
11890
  operationId: "violations.dismiss",
11653
11891
  tags: ["violations"],
11654
11892
  summary: "Dismiss a violation",
11893
+ description: "Admin only. Dismissing is irreversible and refuses violations that have already been handled. Future sightings of the name continue to be accepted but do not flag new violations.",
11655
11894
  security: SESSION_AUTH,
11656
11895
  request: { params: params$1 },
11657
11896
  responses: {
@@ -11709,6 +11948,7 @@ const webhooks = defineModule(defineRoute({
11709
11948
  operationId: "webhooks.update",
11710
11949
  tags: ["webhooks"],
11711
11950
  summary: "Update a webhook",
11951
+ description: "Setting `enabled` to `true` re-enables an auto-disabled webhook and resets its consecutive failure counter to zero. Setting `enabled` to `false` disables the webhook manually.",
11712
11952
  security: SESSION_AUTH,
11713
11953
  request: {
11714
11954
  params,
@@ -11908,7 +12148,7 @@ function registerAuth(program, env, io) {
11908
12148
  if (opts.key) {
11909
12149
  if (!opts.source) throw new Error("`--key` needs `--source <src_...>`: the id you are about to send to is what this checks.");
11910
12150
  const client = createClient({
11911
- baseUrl: resolveApiUrl(env, await readCredentials(env), opts.api, (await readCowConfig(process.cwd()))?.apiUrl),
12151
+ baseUrl: resolveApiUrl(env, await readCredentials(env), opts.api, (await readCowConfig(resolveProjectDir()))?.apiUrl),
11912
12152
  auth: () => opts.key ?? null,
11913
12153
  headers: { [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` }
11914
12154
  });
@@ -11928,7 +12168,7 @@ function registerAuth(program, env, io) {
11928
12168
  }
11929
12169
  const claims = decodeSessionToken(credentials?.token ?? "");
11930
12170
  if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
11931
- const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
12171
+ const projectApiUrl = (await readCowConfig(resolveProjectDir()))?.apiUrl;
11932
12172
  const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
11933
12173
  const mintedFor = credentials?.apiUrl;
11934
12174
  emit({ data: {
@@ -11944,12 +12184,12 @@ function registerAuth(program, env, io) {
11944
12184
  //#endregion
11945
12185
  //#region src/commands/build.ts
11946
12186
  /**
11947
- * `cow build`: typecheck, bundle, and write `.cow/build` for the project in
12187
+ * `cow build`: typecheck, bundle, and write `build/` for the project in
11948
12188
  * the current directory. Local only, no API call and no network.
11949
12189
  */
11950
12190
  function registerBuild(program, io) {
11951
- program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
11952
- emit({ data: await buildProject(process.cwd()) }, io, opts.json);
12191
+ program.command("build").description("typecheck and bundle the repo in the current directory, writing .cowliss/build (local: no API call)").action(async (opts) => {
12192
+ emit({ data: await buildProject(resolveProjectDir()) }, io, opts.json);
11953
12193
  });
11954
12194
  }
11955
12195
 
@@ -12121,6 +12361,7 @@ function cliCommandFor(route, moduleKey) {
12121
12361
  groups: segments.slice(0, -1),
12122
12362
  verb,
12123
12363
  summary: route.summary ?? route.operationId,
12364
+ description: route.description,
12124
12365
  positionals: pathParams(route),
12125
12366
  queryOptions: queryOptions(route),
12126
12367
  bodyOptions: body,
@@ -12135,7 +12376,8 @@ function shapeKeys(schema) {
12135
12376
  function mcpToolFor(route, moduleKey) {
12136
12377
  assertNoHeaders(route);
12137
12378
  const name = route.surfaces?.mcpName ?? route.operationId.replaceAll(".", "_");
12138
- const description = route.surfaces?.description ?? route.summary ?? route.operationId;
12379
+ const summary = route.summary ?? route.operationId;
12380
+ const description = route.surfaces?.description ?? (route.description ? `${summary}\n\n${route.description}` : summary);
12139
12381
  const data = bodyDataSchema(route);
12140
12382
  const rawBody = data !== void 0 && !(data instanceof z.ZodObject);
12141
12383
  const bodyKeys = data === void 0 ? [] : data instanceof z.ZodObject ? Object.keys(data.shape) : ["data"];
@@ -12289,7 +12531,7 @@ function groupFor(program, groups, segments) {
12289
12531
  const segment = segments[depth - 1];
12290
12532
  let group = groups.get(key);
12291
12533
  if (!group) {
12292
- group = parent.command(segment).description(segment);
12534
+ group = parent.command(segment).description(depth === 1 && Object.hasOwn(TAG_DOCS, segment) ? TAG_DOCS[segment].description : segment);
12293
12535
  groups.set(key, group);
12294
12536
  }
12295
12537
  parent = group;
@@ -12303,6 +12545,7 @@ function registerContractCommands(program, run) {
12303
12545
  const requestOf = (client) => client.request;
12304
12546
  for (const spec of plan.cli) {
12305
12547
  const command = groupFor(program, groups, spec.groups).command([spec.verb, ...spec.positionals.map((name) => `<${name}>`)].join(" ")).description(spec.summary);
12548
+ if (spec.description) command.addHelpText("after", `\n${spec.description}`);
12306
12549
  for (const option of [...spec.queryOptions, ...spec.bodyOptions]) registerOption(command, option);
12307
12550
  command.action(async (...args) => {
12308
12551
  const positionals = args.slice(0, spec.positionals.length);
@@ -12424,7 +12667,7 @@ function flipSummary(journeys, status) {
12424
12667
  * segment's own key joins the set the events are filtered by.
12425
12668
  */
12426
12669
  async function unseenForKeys(client, keys) {
12427
- const dir = process.cwd();
12670
+ const dir = resolveProjectDir();
12428
12671
  const project = await assertCowConfig(dir).then(async (config) => ({
12429
12672
  config,
12430
12673
  built: await buildProject(dir)
@@ -12450,7 +12693,7 @@ function register(program, clientFor, io, verb, status) {
12450
12693
  };
12451
12694
  if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome.`);
12452
12695
  if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome.`);
12453
- const selector = merged.all === true ? { appId: (await assertCowConfig(process.cwd())).appId } : { keys };
12696
+ const selector = merged.all === true ? { appId: (await assertCowConfig(resolveProjectDir())).appId } : { keys };
12454
12697
  const client = await clientFor(merged);
12455
12698
  const updated = await setStatus(client, selector, status);
12456
12699
  if (merged.json === true) {
@@ -12467,6 +12710,42 @@ function registerEnable(program, clientFor, io) {
12467
12710
  register(program, clientFor, io, "pause", "paused");
12468
12711
  }
12469
12712
 
12713
+ //#endregion
12714
+ //#region src/commands/events.ts
12715
+ /** `cow events --help` and the `events_prompt` MCP tool, in one place. */
12716
+ const eventsDescription = {
12717
+ cli: "instructions for a coding agent to send the events this app's journeys need",
12718
+ mcp: "Instructions for a coding agent to send the events this app's journeys need. Reads the local project; calls no API."
12719
+ };
12720
+ async function projectEventsPrompt(dir) {
12721
+ const projectDir = dir ?? resolveProjectDir();
12722
+ const { manifest } = await buildProject(projectDir);
12723
+ const items = manifest.events.map((evt) => ({ name: evt.pattern }));
12724
+ return buildEventsPrompt({
12725
+ events: items,
12726
+ sdkPackage: "@cowliss/sdk",
12727
+ trackMethod: "cow.track",
12728
+ referenceUrl: `${DOCS_URL}/sdk/track/`
12729
+ });
12730
+ }
12731
+ function registerEvents(program, io) {
12732
+ let eventsCmd = program.commands.find((c) => c.name() === "events");
12733
+ if (!eventsCmd) eventsCmd = program.command("events");
12734
+ eventsCmd.description(eventsDescription.cli);
12735
+ eventsCmd.action(async (opts) => {
12736
+ const merged = {
12737
+ ...program.opts(),
12738
+ ...opts
12739
+ };
12740
+ const prompt = await projectEventsPrompt();
12741
+ if (merged.json === true) {
12742
+ emit({ data: { prompt } }, io, true);
12743
+ return;
12744
+ }
12745
+ io.stdout(`${prompt}\n`);
12746
+ });
12747
+ }
12748
+
12470
12749
  //#endregion
12471
12750
  //#region src/commands/init.ts
12472
12751
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
@@ -12550,7 +12829,7 @@ async function chooseApp(client, webUrl, flag, org, io, skipPrompts) {
12550
12829
  return { appId: await ask("App id", only ?? "", io, false) };
12551
12830
  }
12552
12831
  function registerInit(program, clientFor, env, io) {
12553
- program.command("init").argument("[dir]", "directory to create the repo in (default: here)").description("create a cow repo: cow.json, journeys/, emails/, segments/, and the files to build them").option("--org <id>", "organization id (default: the logged-in org)").option("--app <id>", "app id this repository pushes to (default: ask, or the org's only one)").option("--yes", "accept the defaults instead of prompting").option("--blueprint <name>", "start from a gallery blueprint").action(async (dirArg, opts) => {
12832
+ program.command("init").argument("[dir]", "repository to create the project in (default: here)").description("create a cow project in .cowliss/: cow.json, journeys/, emails/, segments/, and the files to build them").option("--org <id>", "organization id (default: the logged-in org)").option("--app <id>", "app id this repository pushes to (default: ask, or the org's only one)").option("--yes", "accept the defaults instead of prompting").option("--blueprint <name>", "start from a gallery blueprint").action(async (dirArg, opts) => {
12554
12833
  const merged = {
12555
12834
  ...program.opts(),
12556
12835
  ...opts
@@ -12559,7 +12838,8 @@ function registerInit(program, clientFor, env, io) {
12559
12838
  const cwd = process.cwd();
12560
12839
  const defaultName = basename(dirArg ? resolve(cwd, dirArg) : cwd);
12561
12840
  const name = await ask("Repo name", defaultName, io, skipPrompts);
12562
- const dir = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
12841
+ const repo = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
12842
+ const dir = join(repo, PROJECT_DIR);
12563
12843
  if (existsSync(join(dir, "cow.json"))) throw new Error(`"${dir}" already holds a cow.json. Use \`cow add blueprint\` to add code to it.`);
12564
12844
  const credentials = await readCredentials(env);
12565
12845
  const claimedOrg = credentials ? decodeSessionToken(credentials.token)?.orgId ?? null : null;
@@ -12589,10 +12869,10 @@ function registerInit(program, clientFor, env, io) {
12589
12869
  "journeys",
12590
12870
  "emails",
12591
12871
  "segments",
12592
- ".cow/types.d.ts"
12872
+ "types.d.ts"
12593
12873
  ]
12594
12874
  }),
12595
- ".gitignore": ".cow/\nnode_modules/\n",
12875
+ ".gitignore": "build/\ntypes.d.ts\nnode_modules/\n",
12596
12876
  "journeys/.gitkeep": "",
12597
12877
  "emails/.gitkeep": "",
12598
12878
  "segments/.gitkeep": ""
@@ -12614,7 +12894,7 @@ function registerInit(program, clientFor, env, io) {
12614
12894
  const journey = scenario.slice(10).split(".")[0] ?? "abandoned-checkout";
12615
12895
  const appLine = app.appId === "" ? `No app named yet (${app.reason}). Put its id in cow.json's "appId"; you will find it on the app's page at ${webUrl}/apps.` : `Pushes to ${app.appId}`;
12616
12896
  io.stderr([
12617
- `Created a cow repo in ${here}`,
12897
+ `Created a cow project in ${here}`,
12618
12898
  appLine,
12619
12899
  "",
12620
12900
  "Next steps:",
@@ -12766,8 +13046,8 @@ function registerStatus(program, clientFor, io) {
12766
13046
  ...program.opts(),
12767
13047
  ...opts
12768
13048
  };
12769
- const config = await assertCowConfig(process.cwd());
12770
- const built = await buildProject(process.cwd()).catch(() => null);
13049
+ const config = await assertCowConfig(resolveProjectDir());
13050
+ const built = await buildProject(resolveProjectDir()).catch(() => null);
12771
13051
  const client = await clientFor(merged);
12772
13052
  const status = await readStatus(client, config.appId);
12773
13053
  if (merged.json === true) {
@@ -13005,7 +13285,7 @@ function registerPush(program, clientFor, env, io) {
13005
13285
  ...opts
13006
13286
  };
13007
13287
  const client = await clientFor(merged);
13008
- const projectDir = process.cwd();
13288
+ const projectDir = resolveProjectDir();
13009
13289
  const json = merged.json === true;
13010
13290
  const { plan, built } = await planProject({
13011
13291
  client,
@@ -13069,16 +13349,21 @@ function registerPush(program, clientFor, env, io) {
13069
13349
  * `unseenReport`. There is no second computation that could drift from the
13070
13350
  * one the push itself shows.
13071
13351
  */
13352
+ /** `cow plan --help` and the `pushes_plan` MCP tool, in one place. */
13353
+ const planDescription = {
13354
+ cli: "show what a push of this repository would change, and change nothing",
13355
+ mcp: "What a push of the project in the current directory would change. Builds locally; uploads and stores nothing."
13356
+ };
13072
13357
  /** The envelope `cow plan --json` prints, and what the MCP tool answers. */
13073
13358
  async function projectPlan(client) {
13074
13359
  const { plan } = await planProject({
13075
13360
  client,
13076
- projectDir: process.cwd()
13361
+ projectDir: resolveProjectDir()
13077
13362
  });
13078
13363
  return { data: plan };
13079
13364
  }
13080
13365
  function registerPlan(program, clientFor, io) {
13081
- program.command("plan").description("show what a push of this repository would change, and change nothing").action(async (opts) => {
13366
+ program.command("plan").description(planDescription.cli).action(async (opts) => {
13082
13367
  const merged = {
13083
13368
  ...program.opts(),
13084
13369
  ...opts
@@ -13086,7 +13371,7 @@ function registerPlan(program, clientFor, io) {
13086
13371
  const client = await clientFor(merged);
13087
13372
  const { plan, built } = await planProject({
13088
13373
  client,
13089
- projectDir: process.cwd()
13374
+ projectDir: resolveProjectDir()
13090
13375
  });
13091
13376
  if (merged.json === true) {
13092
13377
  emit({ data: plan }, io, true);
@@ -13179,9 +13464,28 @@ async function createMcpServer({ client }) {
13179
13464
  inputSchema: tool.inputSchema
13180
13465
  }, async (args) => toolResult(() => tool.route.request ? request(tool.route, tool.toInput(args)) : request(tool.route)));
13181
13466
  server.registerTool("pushes_plan", {
13182
- description: "What a push of the project in the current directory would change. Builds locally; uploads and stores nothing.",
13467
+ description: planDescription.mcp,
13183
13468
  inputSchema: {}
13184
13469
  }, async () => toolResult(() => projectPlan(client)));
13470
+ server.registerTool("events_prompt", {
13471
+ description: eventsDescription.mcp,
13472
+ inputSchema: {}
13473
+ }, async () => {
13474
+ try {
13475
+ return { content: [{
13476
+ type: "text",
13477
+ text: await projectEventsPrompt()
13478
+ }] };
13479
+ } catch (error) {
13480
+ return {
13481
+ content: [{
13482
+ type: "text",
13483
+ text: errorMessage$1(error)
13484
+ }],
13485
+ isError: true
13486
+ };
13487
+ }
13488
+ });
13185
13489
  return server;
13186
13490
  }
13187
13491
  function errorMessage$1(error) {
@@ -13234,7 +13538,7 @@ const execFileAsync = promisify(execFile);
13234
13538
  *
13235
13539
  * Untracked files count: a journey the developer wrote and has not committed
13236
13540
  * is exactly what the tarball's copy of the same path would bury, and it is
13237
- * the only copy. `.cow/` and `node_modules/` are gitignored by `cow init`, so
13541
+ * the only copy. `build/` and `node_modules/` are gitignored by `cow init`, so
13238
13542
  * they never show up here.
13239
13543
  */
13240
13544
  async function dirtyPaths(dir) {
@@ -13283,6 +13587,7 @@ async function pullPush({ client, projectDir, pushId, force }) {
13283
13587
  }
13284
13588
  const tarball = await client.request(contract.pushes["pushes.source"], { params: { id: push.id } });
13285
13589
  const files = [];
13590
+ await mkdir(projectDir, { recursive: true });
13286
13591
  const unpack = extract({
13287
13592
  cwd: projectDir,
13288
13593
  onReadEntry: (entry) => files.push(entry.path)
@@ -13305,7 +13610,7 @@ function registerPull(program, clientFor, io) {
13305
13610
  };
13306
13611
  const outcome = await pullPush({
13307
13612
  client: await clientFor(merged),
13308
- projectDir: process.cwd(),
13613
+ projectDir: resolveProjectDir(),
13309
13614
  pushId: opts.push,
13310
13615
  force: opts.force === true
13311
13616
  });
@@ -13560,7 +13865,7 @@ function registerTest(program, io) {
13560
13865
  }
13561
13866
  const parsed = journeyScenarioSchema.safeParse(json);
13562
13867
  if (!parsed.success) throw new Error(`Scenario file "${opts.scenario}" is not a valid scenario: ${firstIssueMessage(parsed.error)}`);
13563
- const projectDir = process.cwd();
13868
+ const projectDir = resolveProjectDir();
13564
13869
  await buildIfStale(projectDir);
13565
13870
  const result = await runScenario(projectDir, journey, parsed.data);
13566
13871
  emit({ data: result }, io, opts.json);
@@ -13585,7 +13890,7 @@ function buildProgram(env, io) {
13585
13890
  const clientFor = async (opts) => {
13586
13891
  const credentials = await readCredentials(env);
13587
13892
  return createClient({
13588
- baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(process.cwd()))?.apiUrl),
13893
+ baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(resolveProjectDir()))?.apiUrl),
13589
13894
  auth: async () => resolveCredential(env, await readCredentials(env)).token,
13590
13895
  headers: async () => ({ [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` })
13591
13896
  });
@@ -13598,6 +13903,7 @@ function buildProgram(env, io) {
13598
13903
  emit(await fn(await clientFor(merged)), io, typeof merged.json === "boolean" ? merged.json : void 0);
13599
13904
  };
13600
13905
  registerContractCommands(program, run);
13906
+ registerEvents(program, io);
13601
13907
  registerAuth(program, env, io);
13602
13908
  registerInit(program, clientFor, env, io);
13603
13909
  registerAdd(program, io);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"