@myapihq/cli 1.1.0-wip.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/{email.d.ts → audience.d.ts} +2 -0
- package/dist/commands/audience.js +245 -0
- package/dist/commands/auth.d.ts +5 -2
- package/dist/commands/auth.js +133 -5
- package/dist/commands/billing.d.ts +2 -0
- package/dist/commands/billing.js +18 -1
- package/dist/commands/company.d.ts +6 -0
- package/dist/commands/company.js +149 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +20 -0
- package/dist/commands/crm/companies.d.ts +6 -0
- package/dist/commands/crm/companies.js +193 -0
- package/dist/commands/crm/contacts.d.ts +6 -0
- package/dist/commands/crm/contacts.js +241 -0
- package/dist/commands/crm/index.d.ts +6 -0
- package/dist/commands/crm/index.js +74 -0
- package/dist/commands/database.d.ts +6 -0
- package/dist/commands/database.js +259 -0
- package/dist/commands/domain.d.ts +3 -0
- package/dist/commands/domain.js +67 -1
- package/dist/commands/email/campaign.d.ts +2 -0
- package/dist/commands/email/campaign.js +34 -1
- package/dist/commands/email/index.d.ts +2 -0
- package/dist/commands/email/index.js +9 -0
- package/dist/commands/email/mailbox.d.ts +2 -0
- package/dist/commands/email/mailbox.js +5 -0
- package/dist/commands/email/message.d.ts +2 -0
- package/dist/commands/email/message.js +8 -0
- package/dist/commands/email/template.d.ts +2 -0
- package/dist/commands/email/template.js +54 -0
- package/dist/commands/email/verify.d.ts +7 -0
- package/dist/commands/email/verify.js +52 -0
- package/dist/commands/email/warmup.d.ts +2 -0
- package/dist/commands/email/warmup.js +7 -0
- package/dist/commands/funnel.d.ts +3 -0
- package/dist/commands/funnel.js +31 -0
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +43 -9
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +5 -0
- package/dist/commands/llm.d.ts +6 -0
- package/dist/commands/llm.js +156 -0
- package/dist/commands/org.d.ts +3 -0
- package/dist/commands/org.js +55 -0
- package/dist/commands/people.d.ts +6 -0
- package/dist/commands/people.js +136 -0
- package/dist/commands/pixel.d.ts +11 -3
- package/dist/commands/pixel.js +93 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +57 -5
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +154 -0
- package/dist/commands/storage.d.ts +2 -0
- package/dist/commands/storage.js +14 -2
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +3 -0
- package/dist/commands/url.d.ts +7 -2
- package/dist/commands/url.js +6 -0
- package/dist/commands/verify.d.ts +7 -0
- package/dist/commands/verify.js +56 -0
- package/dist/commands/webhook.d.ts +2 -0
- package/dist/commands/webhook.js +31 -6
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +19 -1
- package/dist/completion.d.ts +3 -0
- package/dist/completion.js +78 -0
- package/dist/config.d.ts +2 -0
- package/dist/exposes.d.ts +2 -0
- package/dist/exposes.js +13 -0
- package/dist/exposes.test.js +99 -0
- package/dist/flags.test.js +84 -0
- package/dist/index.js +106 -14
- package/dist/prompt.d.ts +15 -0
- package/dist/prompt.js +27 -0
- package/dist/registrant.d.ts +6 -0
- package/dist/registrant.js +155 -0
- package/dist/services-sync.test.d.ts +1 -0
- package/dist/services-sync.test.js +32 -0
- package/dist/skills/my-api-hq/SKILL.md +51 -45
- package/dist/skills/my-crm-api/README.md +42 -0
- package/dist/skills/my-crm-api/SKILL.md +165 -0
- package/dist/skills/my-crm-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-database-api/README.md +41 -0
- package/dist/skills/my-database-api/SKILL.md +107 -0
- package/dist/skills/my-database-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-domain-api/SKILL.md +53 -26
- package/dist/skills/my-email-verify-api/README.md +33 -0
- package/dist/skills/my-email-verify-api/SKILL.md +91 -0
- package/dist/skills/my-email-verify-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-funnel-api/SKILL.md +41 -37
- package/dist/skills/my-image-api/SKILL.md +48 -44
- package/dist/skills/my-llm-api/README.md +35 -0
- package/dist/skills/my-llm-api/SKILL.md +125 -0
- package/dist/skills/my-llm-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-storage-api/README.md +1 -1
- package/dist/skills/my-storage-api/SKILL.md +44 -54
- package/dist/skills/my-webhook-api/README.md +1 -1
- package/dist/skills/my-webhook-api/SKILL.md +48 -92
- package/dist/skills/my-workflow-api/SKILL.md +46 -115
- package/dist/telemetry.d.ts +12 -0
- package/dist/telemetry.js +80 -0
- package/dist/telemetry.test.d.ts +1 -0
- package/dist/telemetry.test.js +101 -0
- package/package.json +13 -8
- package/dist/commands/email.js +0 -586
- package/dist/skills/my-email-api/README.md +0 -45
- package/dist/skills/my-email-api/SKILL.md +0 -104
- package/dist/skills/my-email-api/claude/.claude-plugin/plugin.json +0 -6
- package/dist/skills/my-email-api/make/.gitkeep +0 -0
- package/dist/skills/my-email-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-email-api/openapi/.gitkeep +0 -0
- package/dist/skills/my-funnel-api/make/.gitkeep +0 -0
- package/dist/skills/my-funnel-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-image-api/make/.gitkeep +0 -0
- package/dist/skills/my-image-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-storage-api/make/.gitkeep +0 -0
- package/dist/skills/my-storage-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-webhook-api/make/.gitkeep +0 -0
- package/dist/skills/my-webhook-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-workflow-api/make/.gitkeep +0 -0
- package/dist/skills/my-workflow-api/n8n/.gitkeep +0 -0
- package/dist/utils.test.js +0 -48
- /package/dist/{utils.test.d.ts → exposes.test.d.ts} +0 -0
- /package/dist/skills/{my-api-hq/make → my-crm-api/openapi}/.gitkeep +0 -0
- /package/dist/skills/{my-api-hq/n8n → my-database-api/openapi}/.gitkeep +0 -0
- /package/dist/skills/{my-domain-api/make → my-email-verify-api/openapi}/.gitkeep +0 -0
- /package/dist/skills/{my-domain-api/n8n → my-llm-api/openapi}/.gitkeep +0 -0
|
@@ -1,138 +1,94 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-webhook-api
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: >
|
|
4
|
-
Inbound webhook endpoints. Receive HTTP POSTs from third parties (forms, Stripe, GitHub, etc.) and either inspect
|
|
5
|
+
Inbound webhook endpoints. Receive HTTP POSTs from third parties (forms, Stripe, GitHub, etc.) and either inspect deliveries directly or wire them to a workflow.
|
|
6
|
+
triggers: [webhook, inbound, receiver, form submission, stripe events, slack notification, delivery, payload, event ingest]
|
|
7
|
+
checksum: sha256-pending
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
# MyWebhookAPI
|
|
8
11
|
|
|
9
|
-
Per-org HTTP endpoints that accept inbound POSTs and durably store every delivery. A webhook on its own is a passive receiver — pair it with **myworkflowapi** to react to incoming events
|
|
12
|
+
Per-org HTTP endpoints that accept inbound POSTs and durably store every delivery. A webhook on its own is a passive receiver — pair it with **myworkflowapi** to react to incoming events.
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Capabilities
|
|
15
|
+
<!-- llm:start -->
|
|
16
|
+
Each endpoint has a unique inbound URL minted at create time. Anyone who knows the URL can POST to it; the body is stored verbatim along with headers and a timestamp. Deliveries are kept indefinitely (until you delete the endpoint).
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
- Each endpoint has a unique inbound URL (returned at create time).
|
|
15
|
-
- Deliveries are stored verbatim — full request body + headers — for inspection.
|
|
16
|
-
- `myworkflowapi` workflows bind to a webhook endpoint via its `id`.
|
|
18
|
+
The inbound URL accepts **any JSON body** — no enforced schema, no required fields. Whatever you POST is what gets stored. Two consequences:
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
1. **You decide the shape.** Form submission, Stripe event, custom payload — all work. Be consistent within one webhook so downstream workflows can reference fields reliably.
|
|
21
|
+
2. **Workflows reference fields with `{{ payload.field }}` templating.** If your form POSTs `{"email":"alice@x.com","message":"hi"}`, a workflow step can do `--to "{{ payload.email }}"` to send a reply.
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
myapi webhook create "stripe-events" --description "Stripe payment events"
|
|
23
|
-
# → prints: Inbound URL: https://api.myapihq.com/webhook/in/<slug>
|
|
24
|
-
|
|
25
|
-
# 2. POST to the inbound URL from your service
|
|
26
|
-
curl -X POST https://api.myapihq.com/webhook/in/<slug> \
|
|
27
|
-
-H "Content-Type: application/json" \
|
|
28
|
-
-d '{"event":"payment.succeeded"}'
|
|
29
|
-
|
|
30
|
-
# 3. List endpoints / inspect a delivery
|
|
31
|
-
myapi webhook list
|
|
32
|
-
myapi webhook delivery <delivery_id>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## All Commands
|
|
23
|
+
Treat the webhook slug as a secret — anyone with the URL can POST.
|
|
24
|
+
<!-- llm:end -->
|
|
36
25
|
|
|
26
|
+
## Commands
|
|
27
|
+
<!-- generated:start -->
|
|
37
28
|
| Command | What it does |
|
|
38
29
|
|---|---|
|
|
39
|
-
| `myapi webhook create` | Create an endpoint, returns id + inbound URL |
|
|
30
|
+
| `myapi webhook create "<name>" [--description "..."]` | Create an endpoint, returns id + inbound URL |
|
|
40
31
|
| `myapi webhook list` | List all endpoints in your org |
|
|
41
32
|
| `myapi webhook delete <id>` | Delete an endpoint and all its deliveries |
|
|
42
33
|
| `myapi webhook delivery <delivery_id>` | Get the full payload + headers for one delivery |
|
|
34
|
+
<!-- generated:end -->
|
|
43
35
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
Webhooks become useful when a workflow fires on each inbound POST. See **myworkflowapi**:
|
|
47
|
-
|
|
36
|
+
## Examples
|
|
37
|
+
<!-- llm:start -->
|
|
48
38
|
```bash
|
|
49
|
-
# Create
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Bind a workflow that sends an email each time the webhook fires
|
|
53
|
-
myapi workflow create "Notify on lead" \
|
|
54
|
-
--endpoint-id $WID \
|
|
55
|
-
--steps '[{"type":"send_email","to":"sales@yourdomain.com","subject":"New lead"}]'
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Payload contract
|
|
59
|
-
|
|
60
|
-
The inbound URL accepts **any JSON body** — there's no enforced schema, no
|
|
61
|
-
required fields. Whatever you POST is stored verbatim alongside the headers
|
|
62
|
-
and a timestamp. The shape is whatever the sender chooses to send.
|
|
63
|
-
|
|
64
|
-
That has two consequences:
|
|
39
|
+
# 1. Create a webhook
|
|
40
|
+
myapi webhook create "stripe-events" --description "Stripe payment events"
|
|
41
|
+
# → prints: Inbound URL: https://api.mywebhookapi.com/webhook/in/<slug>
|
|
65
42
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
workflow step can do `--to "{{ payload.email }}"` to send a reply to
|
|
72
|
-
whoever submitted.
|
|
43
|
+
# 2. POST to it from your service
|
|
44
|
+
curl -X POST https://api.mywebhookapi.com/webhook/in/<slug> \
|
|
45
|
+
-H "Content-Type: application/json" \
|
|
46
|
+
-d '{"event":"payment.succeeded"}'
|
|
47
|
+
# → responds with {"delivery_id":"<uuid>"} — capture this to inspect later
|
|
73
48
|
|
|
74
|
-
|
|
75
|
-
|
|
49
|
+
# 3. Inspect a delivery by id (there is no list-deliveries route)
|
|
50
|
+
myapi webhook delivery <delivery_id>
|
|
51
|
+
```
|
|
76
52
|
|
|
77
|
-
|
|
53
|
+
### End-to-end recipe — contact form on your site
|
|
78
54
|
|
|
79
|
-
|
|
80
|
-
thank-you email to them and a Slack ping to your team.
|
|
55
|
+
A contact form on yoursite.com that sends a thank-you email and a Slack ping when submitted:
|
|
81
56
|
|
|
82
57
|
```bash
|
|
83
|
-
# 1. Create a webhook to receive form submissions
|
|
84
58
|
WID=$(myapi webhook create "contact-form" --json | jq -r .id)
|
|
85
59
|
WURL=$(myapi webhook list --json | jq -r ".[] | select(.id==\"$WID\") | .url")
|
|
86
60
|
|
|
87
|
-
# 2. Create the workflow that fires on every submission
|
|
88
61
|
myapi workflow create "Contact form handler" \
|
|
89
62
|
--endpoint-id $WID \
|
|
90
|
-
--steps
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"body": "We got your message and will reply within 24 hours."
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"type": "slack",
|
|
101
|
-
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
102
|
-
"text": "New contact form submission from {{ payload.name }} ({{ payload.email }}): {{ payload.message }}"
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
JSON
|
|
106
|
-
)"
|
|
107
|
-
|
|
108
|
-
# 3. Drop this form in your HTML — it POSTs JSON to the webhook
|
|
109
|
-
echo "<form id='contact'>
|
|
63
|
+
--steps '[
|
|
64
|
+
{"type":"send_email","from":"hello@yoursite.com","to":"{{ payload.email }}","subject":"Thanks, {{ payload.name }}!","body":"We got your message."},
|
|
65
|
+
{"type":"slack","webhook_url":"https://hooks.slack.com/...","text":"New form submission from {{ payload.name }}"}
|
|
66
|
+
]'
|
|
67
|
+
|
|
68
|
+
# Drop this in your HTML — POSTs JSON to $WURL
|
|
69
|
+
echo "<form id='c'>
|
|
110
70
|
<input name='name' required>
|
|
111
71
|
<input name='email' type='email' required>
|
|
112
72
|
<textarea name='message' required></textarea>
|
|
113
73
|
<button>Send</button>
|
|
114
74
|
</form>
|
|
115
75
|
<script>
|
|
116
|
-
document.getElementById('
|
|
76
|
+
document.getElementById('c').onsubmit = async e => {
|
|
117
77
|
e.preventDefault();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
headers: {'Content-Type': 'application/json'},
|
|
122
|
-
body: JSON.stringify(data),
|
|
123
|
-
});
|
|
124
|
-
alert('Thanks! Check your inbox.');
|
|
125
|
-
});
|
|
78
|
+
await fetch('$WURL', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(Object.fromEntries(new FormData(e.target)))});
|
|
79
|
+
alert('Thanks!');
|
|
80
|
+
};
|
|
126
81
|
</script>"
|
|
127
82
|
```
|
|
128
83
|
|
|
129
|
-
The form sends `{
|
|
130
|
-
|
|
84
|
+
The form sends `{name, email, message}` → webhook stores it → workflow runs both steps → email + Slack ping.
|
|
85
|
+
<!-- llm:end -->
|
|
131
86
|
|
|
132
87
|
## Notes
|
|
133
88
|
|
|
134
|
-
- The inbound URL is public — anyone with
|
|
89
|
+
- The inbound URL is public — anyone with it can POST. Treat the slug as a secret.
|
|
135
90
|
- Deliveries are kept indefinitely. Delete the endpoint to purge them.
|
|
136
|
-
- Failed workflow runs
|
|
91
|
+
- Failed workflow runs don't affect the delivery record — the inbound POST is always saved.
|
|
92
|
+
- Inbound responses: `200` on success, `4xx` if the endpoint is missing or body isn't valid JSON.
|
|
137
93
|
|
|
138
94
|
Run `myapi webhook --help` for full flag reference.
|
|
@@ -1,109 +1,83 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-workflow-api
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: >
|
|
4
5
|
Run actions when a webhook fires. Trigger emails, Slack notifications, or HTTP calls in response to inbound webhook deliveries — without writing a backend.
|
|
6
|
+
triggers: [workflow, automation, on webhook, send email on, slack notification, payload templating, drip, trigger, run]
|
|
7
|
+
checksum: sha256-pending
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
# MyWorkflowAPI
|
|
8
11
|
|
|
9
|
-
Workflows bind a list of steps to a webhook endpoint. Every time the webhook receives a POST, the workflow runs its steps in order. Each run is recorded with
|
|
12
|
+
Workflows bind a list of steps to a webhook endpoint. Every time the webhook receives a POST, the workflow runs its steps in order. Each run is recorded with status, attempt count, and any error.
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Capabilities
|
|
15
|
+
<!-- llm:start -->
|
|
16
|
+
A workflow is the "do something when X happens" layer. The trigger is always a **mywebhookapi** endpoint. Steps run sequentially per inbound delivery; failed runs don't block subsequent runs and don't affect the stored webhook delivery.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
- Requires a webhook endpoint id from **mywebhookapi** as the trigger.
|
|
15
|
-
- Steps can reference other services — e.g. send an email via **myemailapi**.
|
|
18
|
+
Step types today:
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
WID=$(myapi webhook create "leads" --json | jq -r .id)
|
|
22
|
-
|
|
23
|
-
# 2. Create a workflow bound to that webhook
|
|
24
|
-
myapi workflow create "Slack on new lead" \
|
|
25
|
-
--endpoint-id $WID \
|
|
26
|
-
--steps '[{"type":"slack","webhook_url":"https://hooks.slack.com/..."}]'
|
|
20
|
+
| Type | Aliases | Required fields |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `send_email` | `email` | `from`, `to`, `subject`, plus one of `body` / `html` / `template_id` |
|
|
23
|
+
| `slack_message` | `slack` | `webhook_url`, `text` |
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
curl -X POST <webhook-inbound-url> -d '{"name":"Alice"}'
|
|
25
|
+
Both step types support **payload templating** with `{{ payload.fieldname }}`. The webhook payload is the entire POST body; named fields are accessed dotted. Whitespace inside braces is fine — both `{{ payload.email }}` and `{{payload.email}}` work.
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
myapi workflow runs <workflow_id>
|
|
33
|
-
myapi workflow get-run <run_id>
|
|
34
|
-
```
|
|
27
|
+
Unknown step types are rejected at create time, so typos surface immediately rather than after 3 failed retries during execution.
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
Workflows can be created `--no-enable` so you can test the flow before going live.
|
|
30
|
+
<!-- llm:end -->
|
|
37
31
|
|
|
32
|
+
## Commands
|
|
33
|
+
<!-- generated:start -->
|
|
38
34
|
| Command | What it does |
|
|
39
35
|
|---|---|
|
|
40
|
-
| `myapi workflow create
|
|
36
|
+
| `myapi workflow create "<name>" --endpoint-id <id> --steps '<json>' [--no-enable]` | Create a workflow |
|
|
41
37
|
| `myapi workflow list` | List workflows in your org |
|
|
42
|
-
| `myapi workflow get <id>` | Inspect
|
|
38
|
+
| `myapi workflow get <id>` | Inspect steps + trigger config |
|
|
43
39
|
| `myapi workflow update <id>` | Change name, endpoint, or steps |
|
|
44
40
|
| `myapi workflow enable <id>` | Enable firing |
|
|
45
41
|
| `myapi workflow disable <id>` | Stop firing without deleting |
|
|
46
42
|
| `myapi workflow delete <id>` | Permanently remove workflow + run history |
|
|
47
43
|
| `myapi workflow runs <id>` | List recent runs (status, attempt, errors) |
|
|
48
44
|
| `myapi workflow get-run <run_id>` | Get full run details + step output |
|
|
45
|
+
<!-- generated:end -->
|
|
49
46
|
|
|
50
|
-
##
|
|
47
|
+
## Examples
|
|
48
|
+
<!-- llm:start -->
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Webhook to act on
|
|
51
|
+
WID=$(myapi webhook create "leads" --json | jq -r .id)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
# 2. Workflow that pings Slack on every inbound POST
|
|
54
|
+
myapi workflow create "Slack on new lead" \
|
|
55
|
+
--endpoint-id $WID \
|
|
56
|
+
--steps '[{"type":"slack","webhook_url":"https://hooks.slack.com/...","text":"new lead!"}]'
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
# 3. Trigger it
|
|
59
|
+
curl -X POST <webhook-inbound-url> -d '{"name":"Alice"}'
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| `slack_message` | `slack` | `webhook_url`, `text` |
|
|
61
|
+
# 4. Inspect runs
|
|
62
|
+
myapi workflow runs <workflow_id>
|
|
63
|
+
myapi workflow get-run <run_id>
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"from": "hello@yourdomain.com",
|
|
66
|
-
"to": "{{ payload.email }}",
|
|
67
|
-
"subject": "Welcome, {{ payload.name }}",
|
|
68
|
-
"template_id": "<template_id>"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"type": "slack",
|
|
72
|
-
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
73
|
-
"text": "New submission from {{ payload.name }}"
|
|
74
|
-
}
|
|
75
|
-
]
|
|
65
|
+
# Test before going live
|
|
66
|
+
myapi workflow create "X" --endpoint-id $WID --steps '...' --no-enable
|
|
67
|
+
# ... iterate ...
|
|
68
|
+
myapi workflow enable <id>
|
|
76
69
|
```
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
immediately instead of after 3 failed retries during execution.
|
|
80
|
-
|
|
81
|
-
The webhook payload is available as `{{ payload }}` and individual fields
|
|
82
|
-
as `{{ payload.fieldname }}`. Whitespace inside the braces is fine —
|
|
83
|
-
both `{{ payload.email }}` and `{{payload.email}}` work.
|
|
84
|
-
|
|
85
|
-
## End-to-end recipe — react to a contact form submission
|
|
86
|
-
|
|
87
|
-
Goal: form on yoursite.com posts to a webhook → workflow sends a thank-you
|
|
88
|
-
email and pings Slack. Full HTML + commands:
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
# 1. Create the webhook to receive submissions
|
|
92
|
-
WID=$(myapi webhook create "contact-form" --json | jq -r .id)
|
|
93
|
-
WURL=$(myapi webhook list --json | jq -r ".[] | select(.id==\"$WID\") | .url")
|
|
71
|
+
### Multi-step example with payload templating
|
|
94
72
|
|
|
95
|
-
|
|
96
|
-
# + Slack ping. Both reference fields from the form payload via {{ }}.
|
|
97
|
-
myapi workflow create "Contact form handler" \
|
|
98
|
-
--endpoint-id $WID \
|
|
99
|
-
--steps "$(cat <<'JSON'
|
|
73
|
+
```json
|
|
100
74
|
[
|
|
101
75
|
{
|
|
102
76
|
"type": "send_email",
|
|
103
77
|
"from": "hello@yoursite.com",
|
|
104
78
|
"to": "{{ payload.email }}",
|
|
105
|
-
"subject": "Thanks
|
|
106
|
-
"
|
|
79
|
+
"subject": "Thanks, {{ payload.name }}",
|
|
80
|
+
"template_id": "<template_id>"
|
|
107
81
|
},
|
|
108
82
|
{
|
|
109
83
|
"type": "slack",
|
|
@@ -111,57 +85,14 @@ myapi workflow create "Contact form handler" \
|
|
|
111
85
|
"text": "New submission from {{ payload.name }} ({{ payload.email }}): {{ payload.message }}"
|
|
112
86
|
}
|
|
113
87
|
]
|
|
114
|
-
JSON
|
|
115
|
-
)"
|
|
116
|
-
|
|
117
|
-
# 3. The HTML form on yoursite.com — POSTs JSON to the webhook
|
|
118
|
-
echo "<form id='contact'>
|
|
119
|
-
<input name='name' required>
|
|
120
|
-
<input name='email' type='email' required>
|
|
121
|
-
<textarea name='message' required></textarea>
|
|
122
|
-
<button>Send</button>
|
|
123
|
-
</form>
|
|
124
|
-
<script>
|
|
125
|
-
document.getElementById('contact').addEventListener('submit', async e => {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
const data = Object.fromEntries(new FormData(e.target));
|
|
128
|
-
await fetch('$WURL', {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: {'Content-Type': 'application/json'},
|
|
131
|
-
body: JSON.stringify(data),
|
|
132
|
-
});
|
|
133
|
-
alert('Thanks! Check your inbox.');
|
|
134
|
-
});
|
|
135
|
-
</script>"
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
The form sends `{"name": "...", "email": "...", "message": "..."}`. The
|
|
139
|
-
webhook stores it. The workflow runs both steps in order: the user gets
|
|
140
|
-
an email at the address they typed, your team gets a Slack ping with the
|
|
141
|
-
message. Each invocation is recorded — `myapi workflow runs <id>` shows
|
|
142
|
-
the history.
|
|
143
|
-
|
|
144
|
-
## Lifecycle
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
# Create disabled, test, then enable
|
|
148
|
-
myapi workflow create "X" --endpoint-id $WID --steps '...' --no-enable
|
|
149
|
-
|
|
150
|
-
# Trigger via webhook POST, inspect
|
|
151
|
-
myapi workflow runs <id>
|
|
152
|
-
|
|
153
|
-
# When ready
|
|
154
|
-
myapi workflow enable <id>
|
|
155
|
-
|
|
156
|
-
# To temporarily stop
|
|
157
|
-
myapi workflow disable <id>
|
|
158
88
|
```
|
|
89
|
+
<!-- llm:end -->
|
|
159
90
|
|
|
160
91
|
## Notes
|
|
161
92
|
|
|
162
93
|
- Workflows fire on every webhook delivery. Failed runs don't block subsequent runs.
|
|
163
|
-
- Each run records
|
|
164
|
-
-
|
|
94
|
+
- Each run records attempt number, started_at/finished_at, and any error.
|
|
95
|
+
- Disable to stop firing without losing the configuration.
|
|
165
96
|
- Deleting a workflow purges its run history; the webhook endpoint remains.
|
|
166
97
|
|
|
167
98
|
Run `myapi workflow --help` for full flag reference.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare function resolveHqBase(): string;
|
|
2
|
+
export interface SkillsInstallEvent {
|
|
3
|
+
agents_targeted: string[];
|
|
4
|
+
cli_version: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function emitSkillsInstall(agentsTargeted: string[], cliVersion: string): void;
|
|
7
|
+
export declare const __TELEMETRY_INTERNAL__: {
|
|
8
|
+
resolveHqBase: typeof resolveHqBase;
|
|
9
|
+
ENDPOINT_PATH: string;
|
|
10
|
+
TIMEOUT_MS: number;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Telemetry client — emits the single `skills.install` event to the backend
|
|
2
|
+
// so the north-star metric (skills installations) can be aggregated.
|
|
3
|
+
//
|
|
4
|
+
// Design (per integrations PRD FR-013 + architecture Subsystem C):
|
|
5
|
+
// - Single event: only fires when `myapi install-skills` succeeds.
|
|
6
|
+
// Every other meaningful CLI operation already calls the backend
|
|
7
|
+
// under the user's auth, so backend access logs cover those for free.
|
|
8
|
+
// - Authenticated: bearer-token from ~/.myapi/config. If the user
|
|
9
|
+
// isn't authenticated, silently skip (no anonymous events).
|
|
10
|
+
// - Fire-and-forget: 200 ms timeout, `req.unref()` so it never blocks
|
|
11
|
+
// process exit. Errors swallowed.
|
|
12
|
+
// - No PII: only `agents_targeted` (a closed-set array of "claude" |
|
|
13
|
+
// "gemini" | "cursor") and the CLI version. The bearer already
|
|
14
|
+
// identifies the account; the backend handles any anonymization.
|
|
15
|
+
import * as http from 'node:http';
|
|
16
|
+
import * as https from 'node:https';
|
|
17
|
+
import { URL } from 'node:url';
|
|
18
|
+
import { loadConfig } from './config.js';
|
|
19
|
+
// Resolved per-call so tests / local dev can override via env var without
|
|
20
|
+
// having to reset the module. Default matches the SDK's HQ_BASE convention.
|
|
21
|
+
function resolveHqBase() {
|
|
22
|
+
return process.env.MYAPI_HQ_URL ?? process.env.MYAPI_API_BASE ?? 'https://api.myapihq.com';
|
|
23
|
+
}
|
|
24
|
+
const ENDPOINT_PATH = '/v1/integrations/skills-install';
|
|
25
|
+
const TIMEOUT_MS = 200;
|
|
26
|
+
// Emit the event. Returns immediately; the request runs in the background
|
|
27
|
+
// (unref'd) and silently dies on error/timeout. Never throws.
|
|
28
|
+
export function emitSkillsInstall(agentsTargeted, cliVersion) {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
if (!config?.api_key)
|
|
31
|
+
return; // not authenticated → no event, by design
|
|
32
|
+
let target;
|
|
33
|
+
try {
|
|
34
|
+
target = new URL(ENDPOINT_PATH, resolveHqBase());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
} // bad URL → silently no-op
|
|
39
|
+
const lib = target.protocol === 'https:' ? https : http;
|
|
40
|
+
const payload = {
|
|
41
|
+
agents_targeted: agentsTargeted,
|
|
42
|
+
cli_version: cliVersion,
|
|
43
|
+
};
|
|
44
|
+
const body = JSON.stringify(payload);
|
|
45
|
+
let req;
|
|
46
|
+
try {
|
|
47
|
+
req = lib.request({
|
|
48
|
+
hostname: target.hostname,
|
|
49
|
+
port: target.port ? Number(target.port) : (target.protocol === 'https:' ? 443 : 80),
|
|
50
|
+
path: target.pathname,
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
authorization: `Bearer ${config.api_key}`,
|
|
54
|
+
'content-type': 'application/json',
|
|
55
|
+
'content-length': Buffer.byteLength(body),
|
|
56
|
+
},
|
|
57
|
+
timeout: TIMEOUT_MS,
|
|
58
|
+
},
|
|
59
|
+
// We don't care about the response; consume it so the socket can close.
|
|
60
|
+
(res) => { res.resume(); });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return; // hostname parse error etc.
|
|
64
|
+
}
|
|
65
|
+
req.on('error', () => { });
|
|
66
|
+
req.on('timeout', () => req.destroy());
|
|
67
|
+
// unref the underlying socket so the request never blocks process exit.
|
|
68
|
+
// ClientRequest itself doesn't expose unref(); the socket does.
|
|
69
|
+
req.on('socket', (socket) => { try {
|
|
70
|
+
socket.unref();
|
|
71
|
+
}
|
|
72
|
+
catch { } });
|
|
73
|
+
req.end(body);
|
|
74
|
+
}
|
|
75
|
+
// Exposed for tests.
|
|
76
|
+
export const __TELEMETRY_INTERNAL__ = {
|
|
77
|
+
resolveHqBase,
|
|
78
|
+
ENDPOINT_PATH,
|
|
79
|
+
TIMEOUT_MS,
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
|
+
// Mock the config module so we can inject api_key per-test without
|
|
4
|
+
// touching the real ~/.myapi/config.json.
|
|
5
|
+
let mockedConfig = null;
|
|
6
|
+
vi.mock('./config.js', () => ({
|
|
7
|
+
loadConfig: () => mockedConfig,
|
|
8
|
+
}));
|
|
9
|
+
import { emitSkillsInstall } from './telemetry.js';
|
|
10
|
+
function withTestServer() {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const received = [];
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
req.on('data', (c) => chunks.push(c));
|
|
16
|
+
req.on('end', () => {
|
|
17
|
+
received.push({
|
|
18
|
+
method: req.method ?? '',
|
|
19
|
+
url: req.url ?? '',
|
|
20
|
+
headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : (v ?? '')])),
|
|
21
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
22
|
+
});
|
|
23
|
+
res.statusCode = 204;
|
|
24
|
+
res.end();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
server.listen(0, '127.0.0.1', () => {
|
|
28
|
+
const addr = server.address();
|
|
29
|
+
if (!addr || typeof addr === 'string')
|
|
30
|
+
throw new Error('bad address');
|
|
31
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
32
|
+
resolve({
|
|
33
|
+
url,
|
|
34
|
+
received,
|
|
35
|
+
close: () => server.close(),
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function wait(ms) {
|
|
41
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
describe('emitSkillsInstall', () => {
|
|
44
|
+
let originalHqUrl;
|
|
45
|
+
let originalApiBase;
|
|
46
|
+
let server;
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
server = await withTestServer();
|
|
49
|
+
originalHqUrl = process.env.MYAPI_HQ_URL;
|
|
50
|
+
originalApiBase = process.env.MYAPI_API_BASE;
|
|
51
|
+
process.env.MYAPI_HQ_URL = server.url;
|
|
52
|
+
delete process.env.MYAPI_API_BASE;
|
|
53
|
+
mockedConfig = null;
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
server.close();
|
|
57
|
+
if (originalHqUrl === undefined)
|
|
58
|
+
delete process.env.MYAPI_HQ_URL;
|
|
59
|
+
else
|
|
60
|
+
process.env.MYAPI_HQ_URL = originalHqUrl;
|
|
61
|
+
if (originalApiBase === undefined)
|
|
62
|
+
delete process.env.MYAPI_API_BASE;
|
|
63
|
+
else
|
|
64
|
+
process.env.MYAPI_API_BASE = originalApiBase;
|
|
65
|
+
});
|
|
66
|
+
it('posts to /v1/integrations/skills-install with bearer + JSON body when authenticated', async () => {
|
|
67
|
+
mockedConfig = { api_key: 'hq_live_TESTKEY' };
|
|
68
|
+
emitSkillsInstall(['claude', 'gemini'], '1.2.3');
|
|
69
|
+
await wait(150);
|
|
70
|
+
expect(server.received).toHaveLength(1);
|
|
71
|
+
const r = server.received[0];
|
|
72
|
+
expect(r.method).toBe('POST');
|
|
73
|
+
expect(r.url).toBe('/v1/integrations/skills-install');
|
|
74
|
+
expect(r.headers.authorization).toBe('Bearer hq_live_TESTKEY');
|
|
75
|
+
expect(r.headers['content-type']).toBe('application/json');
|
|
76
|
+
const body = JSON.parse(r.body);
|
|
77
|
+
expect(body).toEqual({ agents_targeted: ['claude', 'gemini'], cli_version: '1.2.3' });
|
|
78
|
+
});
|
|
79
|
+
it('silently skips emission when config has no api_key', async () => {
|
|
80
|
+
mockedConfig = null; // no config
|
|
81
|
+
emitSkillsInstall(['claude'], '1.2.3');
|
|
82
|
+
await wait(50);
|
|
83
|
+
expect(server.received).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
it('silently skips emission when config has empty api_key', async () => {
|
|
86
|
+
mockedConfig = {};
|
|
87
|
+
emitSkillsInstall(['claude'], '1.2.3');
|
|
88
|
+
await wait(50);
|
|
89
|
+
expect(server.received).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
it('does not throw on connection failure (fire-and-forget guarantee)', () => {
|
|
92
|
+
mockedConfig = { api_key: 'hq_live_TESTKEY' };
|
|
93
|
+
process.env.MYAPI_HQ_URL = 'http://127.0.0.1:1'; // unreachable
|
|
94
|
+
expect(() => emitSkillsInstall(['claude'], '1.2.3')).not.toThrow();
|
|
95
|
+
});
|
|
96
|
+
it('does not throw on malformed URL', () => {
|
|
97
|
+
mockedConfig = { api_key: 'hq_live_TESTKEY' };
|
|
98
|
+
process.env.MYAPI_HQ_URL = 'not-a-url';
|
|
99
|
+
expect(() => emitSkillsInstall(['claude'], '1.2.3')).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
|
-
"
|
|
3
|
+
"license": "Apache-2.0",
|
|
4
|
+
"version": "1.2.0",
|
|
4
5
|
"description": "MyAPI command-line interface",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"files": [
|
|
@@ -14,20 +15,24 @@
|
|
|
14
15
|
"prebuild": "node scripts/copy-skills.js",
|
|
15
16
|
"build": "tsc && rm -rf dist/skills && cp -r src/skills dist/skills",
|
|
16
17
|
"dev": "tsc --watch",
|
|
17
|
-
"test": "vitest run src",
|
|
18
|
-
"test:smoke": "npm run build && vitest run src test/smoke",
|
|
18
|
+
"test": "vitest run src test/scripts",
|
|
19
|
+
"test:smoke": "npm run build && vitest run src test/smoke test/scripts",
|
|
19
20
|
"test:online": "npm run build && MYAPI_RUN_RESET=1 vitest run test/online",
|
|
20
|
-
"test:all": "npm run build && MYAPI_RUN_RESET=1 vitest run src test/smoke test/online"
|
|
21
|
+
"test:all": "npm run build && MYAPI_RUN_RESET=1 vitest run src test/smoke test/scripts test/online",
|
|
22
|
+
"check-coverage": "npm run build && node scripts/check-coverage.js",
|
|
23
|
+
"check-coverage:live": "npm run build && node scripts/check-coverage.js --live",
|
|
24
|
+
"update-schema": "node scripts/update-schema.js",
|
|
25
|
+
"lint:skills": "node scripts/lint-skills.js",
|
|
26
|
+
"lint:skills:strict": "node scripts/lint-skills.js --strict",
|
|
27
|
+
"lint:changelog": "node ../../scripts/lint-changelog.js"
|
|
21
28
|
},
|
|
22
29
|
"dependencies": {
|
|
23
|
-
"@myapihq/sdk": "1.
|
|
24
|
-
"omelette": "^0.4.17"
|
|
25
|
-
"update-notifier": "^7.3.1"
|
|
30
|
+
"@myapihq/sdk": "^1.2.0",
|
|
31
|
+
"omelette": "^0.4.17"
|
|
26
32
|
},
|
|
27
33
|
"devDependencies": {
|
|
28
34
|
"@types/node": "^25.6.0",
|
|
29
35
|
"@types/omelette": "^0.4.5",
|
|
30
|
-
"@types/update-notifier": "^6.0.8",
|
|
31
36
|
"typescript": "^5.4.0",
|
|
32
37
|
"vitest": "^4.1.5"
|
|
33
38
|
}
|