@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.
- package/blueprints/abandoned-checkout/events.json +9 -0
- package/blueprints/activity-decay/events.json +1 -0
- package/blueprints/ecommerce/events.json +28 -0
- package/blueprints/mobile-game/events.json +22 -0
- package/blueprints/saas-trial/events.json +18 -0
- package/blueprints/winback/events.json +6 -0
- package/dist/guest/{driver-B_pjmy5g.js → driver-BtQYaFXP.js} +1 -1
- package/dist/guest/driver-emails.js +2 -2
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{emails-5GindgMQ.d.ts → emails-DUwxKLGO.d.ts} +1 -1
- package/dist/guest/emails.d.ts +1 -1
- package/dist/guest/{journeys-v2-Djgs8U91.js → journeys-v2-Cndh4CAr.js} +1 -1
- package/dist/guest/journeys.d.ts +1 -1
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/segments.js +1 -1
- package/dist/index.js +361 -55
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-
|
|
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-
|
|
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-
|
|
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
|
|
package/dist/guest/driver.d.ts
CHANGED
|
@@ -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-
|
|
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
|
package/dist/guest/driver.js
CHANGED
|
@@ -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
|
|
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> = {
|
package/dist/guest/emails.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as Template, r as SendClass, t as Subject } from "./emails-
|
|
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 `.
|
|
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
|
package/dist/guest/journeys.d.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/dist/guest/journeys.js
CHANGED
package/dist/guest/segments.js
CHANGED
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 `.
|
|
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({
|
|
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 =
|
|
8522
|
-
const TYPES_FILE =
|
|
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
|
-
*
|
|
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("
|
|
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
|
|
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
|
-
*
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
11204
|
-
*
|
|
11205
|
-
*
|
|
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/
|
|
11425
|
+
//#region ../../packages/shared/src/contract/tags.ts
|
|
11334
11426
|
/**
|
|
11335
|
-
*
|
|
11336
|
-
*
|
|
11337
|
-
*
|
|
11338
|
-
*
|
|
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
|
-
*
|
|
11342
|
-
*
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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 .
|
|
11952
|
-
emit({ data: await buildProject(
|
|
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
|
|
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 =
|
|
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(
|
|
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]", "
|
|
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
|
|
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
|
-
"
|
|
12872
|
+
"types.d.ts"
|
|
12593
12873
|
]
|
|
12594
12874
|
}),
|
|
12595
|
-
".gitignore": ".
|
|
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
|
|
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(
|
|
12770
|
-
const built = await buildProject(
|
|
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 =
|
|
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:
|
|
13361
|
+
projectDir: resolveProjectDir()
|
|
13077
13362
|
});
|
|
13078
13363
|
return { data: plan };
|
|
13079
13364
|
}
|
|
13080
13365
|
function registerPlan(program, clientFor, io) {
|
|
13081
|
-
program.command("plan").description(
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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 =
|
|
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(
|
|
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);
|