@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.
Files changed (127) hide show
  1. package/dist/commands/{email.d.ts → audience.d.ts} +2 -0
  2. package/dist/commands/audience.js +245 -0
  3. package/dist/commands/auth.d.ts +5 -2
  4. package/dist/commands/auth.js +133 -5
  5. package/dist/commands/billing.d.ts +2 -0
  6. package/dist/commands/billing.js +18 -1
  7. package/dist/commands/company.d.ts +6 -0
  8. package/dist/commands/company.js +149 -0
  9. package/dist/commands/config.d.ts +2 -0
  10. package/dist/commands/config.js +20 -0
  11. package/dist/commands/crm/companies.d.ts +6 -0
  12. package/dist/commands/crm/companies.js +193 -0
  13. package/dist/commands/crm/contacts.d.ts +6 -0
  14. package/dist/commands/crm/contacts.js +241 -0
  15. package/dist/commands/crm/index.d.ts +6 -0
  16. package/dist/commands/crm/index.js +74 -0
  17. package/dist/commands/database.d.ts +6 -0
  18. package/dist/commands/database.js +259 -0
  19. package/dist/commands/domain.d.ts +3 -0
  20. package/dist/commands/domain.js +67 -1
  21. package/dist/commands/email/campaign.d.ts +2 -0
  22. package/dist/commands/email/campaign.js +34 -1
  23. package/dist/commands/email/index.d.ts +2 -0
  24. package/dist/commands/email/index.js +9 -0
  25. package/dist/commands/email/mailbox.d.ts +2 -0
  26. package/dist/commands/email/mailbox.js +5 -0
  27. package/dist/commands/email/message.d.ts +2 -0
  28. package/dist/commands/email/message.js +8 -0
  29. package/dist/commands/email/template.d.ts +2 -0
  30. package/dist/commands/email/template.js +54 -0
  31. package/dist/commands/email/verify.d.ts +7 -0
  32. package/dist/commands/email/verify.js +52 -0
  33. package/dist/commands/email/warmup.d.ts +2 -0
  34. package/dist/commands/email/warmup.js +7 -0
  35. package/dist/commands/funnel.d.ts +3 -0
  36. package/dist/commands/funnel.js +31 -0
  37. package/dist/commands/image.d.ts +2 -0
  38. package/dist/commands/image.js +43 -9
  39. package/dist/commands/keys.d.ts +2 -0
  40. package/dist/commands/keys.js +5 -0
  41. package/dist/commands/llm.d.ts +6 -0
  42. package/dist/commands/llm.js +156 -0
  43. package/dist/commands/org.d.ts +3 -0
  44. package/dist/commands/org.js +55 -0
  45. package/dist/commands/people.d.ts +6 -0
  46. package/dist/commands/people.js +136 -0
  47. package/dist/commands/pixel.d.ts +11 -3
  48. package/dist/commands/pixel.js +93 -0
  49. package/dist/commands/setup.d.ts +2 -0
  50. package/dist/commands/setup.js +57 -5
  51. package/dist/commands/status.d.ts +7 -0
  52. package/dist/commands/status.js +154 -0
  53. package/dist/commands/storage.d.ts +2 -0
  54. package/dist/commands/storage.js +14 -2
  55. package/dist/commands/update.d.ts +2 -0
  56. package/dist/commands/update.js +3 -0
  57. package/dist/commands/url.d.ts +7 -2
  58. package/dist/commands/url.js +6 -0
  59. package/dist/commands/verify.d.ts +7 -0
  60. package/dist/commands/verify.js +56 -0
  61. package/dist/commands/webhook.d.ts +2 -0
  62. package/dist/commands/webhook.js +31 -6
  63. package/dist/commands/workflow.d.ts +2 -0
  64. package/dist/commands/workflow.js +19 -1
  65. package/dist/completion.d.ts +3 -0
  66. package/dist/completion.js +78 -0
  67. package/dist/config.d.ts +2 -0
  68. package/dist/exposes.d.ts +2 -0
  69. package/dist/exposes.js +13 -0
  70. package/dist/exposes.test.js +99 -0
  71. package/dist/flags.test.js +84 -0
  72. package/dist/index.js +106 -14
  73. package/dist/prompt.d.ts +15 -0
  74. package/dist/prompt.js +27 -0
  75. package/dist/registrant.d.ts +6 -0
  76. package/dist/registrant.js +155 -0
  77. package/dist/services-sync.test.d.ts +1 -0
  78. package/dist/services-sync.test.js +32 -0
  79. package/dist/skills/my-api-hq/SKILL.md +51 -45
  80. package/dist/skills/my-crm-api/README.md +42 -0
  81. package/dist/skills/my-crm-api/SKILL.md +165 -0
  82. package/dist/skills/my-crm-api/claude/.claude-plugin/plugin.json +6 -0
  83. package/dist/skills/my-database-api/README.md +41 -0
  84. package/dist/skills/my-database-api/SKILL.md +107 -0
  85. package/dist/skills/my-database-api/claude/.claude-plugin/plugin.json +6 -0
  86. package/dist/skills/my-domain-api/SKILL.md +53 -26
  87. package/dist/skills/my-email-verify-api/README.md +33 -0
  88. package/dist/skills/my-email-verify-api/SKILL.md +91 -0
  89. package/dist/skills/my-email-verify-api/claude/.claude-plugin/plugin.json +6 -0
  90. package/dist/skills/my-funnel-api/SKILL.md +41 -37
  91. package/dist/skills/my-image-api/SKILL.md +48 -44
  92. package/dist/skills/my-llm-api/README.md +35 -0
  93. package/dist/skills/my-llm-api/SKILL.md +125 -0
  94. package/dist/skills/my-llm-api/claude/.claude-plugin/plugin.json +6 -0
  95. package/dist/skills/my-storage-api/README.md +1 -1
  96. package/dist/skills/my-storage-api/SKILL.md +44 -54
  97. package/dist/skills/my-webhook-api/README.md +1 -1
  98. package/dist/skills/my-webhook-api/SKILL.md +48 -92
  99. package/dist/skills/my-workflow-api/SKILL.md +46 -115
  100. package/dist/telemetry.d.ts +12 -0
  101. package/dist/telemetry.js +80 -0
  102. package/dist/telemetry.test.d.ts +1 -0
  103. package/dist/telemetry.test.js +101 -0
  104. package/package.json +13 -8
  105. package/dist/commands/email.js +0 -586
  106. package/dist/skills/my-email-api/README.md +0 -45
  107. package/dist/skills/my-email-api/SKILL.md +0 -104
  108. package/dist/skills/my-email-api/claude/.claude-plugin/plugin.json +0 -6
  109. package/dist/skills/my-email-api/make/.gitkeep +0 -0
  110. package/dist/skills/my-email-api/n8n/.gitkeep +0 -0
  111. package/dist/skills/my-email-api/openapi/.gitkeep +0 -0
  112. package/dist/skills/my-funnel-api/make/.gitkeep +0 -0
  113. package/dist/skills/my-funnel-api/n8n/.gitkeep +0 -0
  114. package/dist/skills/my-image-api/make/.gitkeep +0 -0
  115. package/dist/skills/my-image-api/n8n/.gitkeep +0 -0
  116. package/dist/skills/my-storage-api/make/.gitkeep +0 -0
  117. package/dist/skills/my-storage-api/n8n/.gitkeep +0 -0
  118. package/dist/skills/my-webhook-api/make/.gitkeep +0 -0
  119. package/dist/skills/my-webhook-api/n8n/.gitkeep +0 -0
  120. package/dist/skills/my-workflow-api/make/.gitkeep +0 -0
  121. package/dist/skills/my-workflow-api/n8n/.gitkeep +0 -0
  122. package/dist/utils.test.js +0 -48
  123. /package/dist/{utils.test.d.ts → exposes.test.d.ts} +0 -0
  124. /package/dist/skills/{my-api-hq/make → my-crm-api/openapi}/.gitkeep +0 -0
  125. /package/dist/skills/{my-api-hq/n8n → my-database-api/openapi}/.gitkeep +0 -0
  126. /package/dist/skills/{my-domain-api/make → my-email-verify-api/openapi}/.gitkeep +0 -0
  127. /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 the deliveries directly or wire them to a workflow.
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 (send email, post to Slack, etc.).
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
- ## How It Fits Together
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
- - Requires `api_key` and `org_id` from **myapihq**.
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
- ## Quick Start
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
- ```bash
21
- # 1. Create a webhook endpoint
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
- ## Wiring to a Workflow
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 the webhook
50
- WID=$(myapi webhook create "leads" --json | jq -r .id)
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
- 1. **You decide the shape.** A form, a Stripe event, a custom payload — all
67
- work. Be consistent within one webhook so downstream workflows can
68
- reference fields reliably.
69
- 2. **Workflows reference fields with `{{ payload.field }}` templating.**
70
- If your form POSTs `{"email": "alice@x.com", "message": "hi"}`, a
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
- Inbound responses: 200 OK on success, 4xx if the endpoint is missing or
75
- the body isn't valid JSON.
49
+ # 3. Inspect a delivery by id (there is no list-deliveries route)
50
+ myapi webhook delivery <delivery_id>
51
+ ```
76
52
 
77
- ## End-to-end recipe — contact form on your website
53
+ ### End-to-end recipe — contact form on your site
78
54
 
79
- Goal: a contact form on yoursite.com. When someone submits, send a
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 "$(cat <<'JSON'
91
- [
92
- {
93
- "type": "send_email",
94
- "from": "hello@yoursite.com",
95
- "to": "{{ payload.email }}",
96
- "subject": "Thanks for reaching out, {{ payload.name }}!",
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('contact').addEventListener('submit', async e => {
76
+ document.getElementById('c').onsubmit = async e => {
117
77
  e.preventDefault();
118
- const data = Object.fromEntries(new FormData(e.target));
119
- await fetch('$WURL', {
120
- method: 'POST',
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 `{"name": "...", "email": "...", "message": "..."}` →
130
- webhook stores it → workflow fires → email goes out → Slack gets pinged.
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 the URL can POST to it. Treat the slug as a secret.
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 do not affect the delivery record — the inbound POST is always saved.
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 its status, attempt count, and any error.
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
- ## How It Fits Together
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
- - Requires `api_key` and `org_id` from **myapihq**.
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
- ## Quick Start
18
-
19
- ```bash
20
- # 1. Create the webhook that will trigger the workflow
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
- # 3. POST to the webhook workflow fires
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
- # 4. Inspect runs
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
- ## All Commands
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` | Create a new workflow (`--name --endpoint-id --steps`) |
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 a workflow's steps + trigger config |
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
- ## Steps
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
- `--steps` is a JSON array of step objects. Each step has a `type` and type-specific fields.
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
- Supported step types (alias forms in parentheses):
58
+ # 3. Trigger it
59
+ curl -X POST <webhook-inbound-url> -d '{"name":"Alice"}'
55
60
 
56
- | `type` | Aliases | Required fields |
57
- |---|---|---|
58
- | `send_email` | `email` | `from`, `to`, `subject`, plus one of `body` / `html` / `template_id` |
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
- ```json
62
- [
63
- {
64
- "type": "email",
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
- Unknown step types are rejected at workflow create time, so typos surface
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
- # 2. Workflow that fires on each submission. Two steps: thank-you email
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 for reaching out, {{ payload.name }}!",
106
- "body": "We got your message and will reply within 24 hours."
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 its attempt number, started_at/finished_at, and any error.
164
- - A workflow can be disabled to stop firing without losing the configuration.
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
- "version": "1.1.0-wip.4",
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.1.0-wip.0",
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
  }