@myapihq/cli 1.1.0-wip.0 → 1.1.0-wip.2

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 (43) hide show
  1. package/dist/commands/auth.js +75 -33
  2. package/dist/commands/billing.js +3 -5
  3. package/dist/commands/domain.js +8 -1
  4. package/dist/commands/email/campaign.d.ts +2 -0
  5. package/dist/commands/email/campaign.js +152 -0
  6. package/dist/commands/email/index.d.ts +4 -0
  7. package/dist/commands/email/index.js +98 -0
  8. package/dist/commands/email/mailbox.d.ts +2 -0
  9. package/dist/commands/email/mailbox.js +88 -0
  10. package/dist/commands/email/message.d.ts +2 -0
  11. package/dist/commands/email/message.js +115 -0
  12. package/dist/commands/email/template.d.ts +2 -0
  13. package/dist/commands/email/template.js +106 -0
  14. package/dist/commands/email/warmup.d.ts +2 -0
  15. package/dist/commands/email/warmup.js +43 -0
  16. package/dist/commands/email.js +71 -51
  17. package/dist/commands/funnel.d.ts +1 -1
  18. package/dist/commands/funnel.js +31 -9
  19. package/dist/commands/keys.d.ts +1 -1
  20. package/dist/commands/keys.js +6 -9
  21. package/dist/commands/org.js +17 -34
  22. package/dist/commands/setup.js +143 -115
  23. package/dist/commands/update.js +5 -0
  24. package/dist/commands/webhook.d.ts +1 -1
  25. package/dist/commands/webhook.js +23 -7
  26. package/dist/commands/workflow.d.ts +1 -1
  27. package/dist/commands/workflow.js +45 -6
  28. package/dist/config.js +10 -5
  29. package/dist/helpers.js +8 -6
  30. package/dist/index.js +9 -3
  31. package/dist/output.d.ts +3 -1
  32. package/dist/output.js +12 -0
  33. package/dist/prompt.d.ts +24 -0
  34. package/dist/prompt.js +41 -0
  35. package/dist/skills/my-email-api/README.md +1 -1
  36. package/dist/skills/my-email-api/SKILL.md +4 -6
  37. package/dist/skills/my-webhook-api/README.md +1 -1
  38. package/dist/skills/my-webhook-api/SKILL.md +77 -4
  39. package/dist/skills/my-workflow-api/README.md +3 -4
  40. package/dist/skills/my-workflow-api/SKILL.md +79 -10
  41. package/dist/utils.d.ts +26 -0
  42. package/dist/utils.js +32 -0
  43. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- import * as readline from 'readline';
2
1
  import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
3
2
  import { requireConfig, saveConfig } from '../config.js';
4
3
  import { success, error, printTable, printJson, info } from '../output.js';
5
- import { sleep, formatDate } from '../utils.js';
6
- import { requireOrg } from '../helpers.js';
4
+ import { confirm } from '../prompt.js';
5
+ import { formatDate, pollJob } from '../utils.js';
6
+ import { requireOrg, requireArg } from '../helpers.js';
7
7
  export const SCHEMA = {
8
8
  name: 'string',
9
9
  tagline: 'string',
@@ -34,13 +34,7 @@ export async function create(restArgs, flags) {
34
34
  info(`Preview domain: https://${org.preview_subdomain}.makeautonomous.com`);
35
35
  }
36
36
  }
37
- const setDefault = flags.yes || await new Promise(resolve => {
38
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
39
- rl.question('› Set as default org and funnel? (Y/n) ', ans => {
40
- rl.close();
41
- resolve(ans.trim().toLowerCase() !== 'n');
42
- });
43
- });
37
+ const setDefault = !!flags.yes || await confirm('› Set as default org and funnel? (Y/n) ', true);
44
38
  let funnelId;
45
39
  if (setDefault) {
46
40
  config.default_org = org.id;
@@ -108,8 +102,7 @@ export async function get(id, flags) {
108
102
  info(`Preview: https://${org.preview_subdomain}.makeautonomous.com`);
109
103
  }
110
104
  export async function del(id, _flags) {
111
- if (!id)
112
- error('Missing org id. Usage: myapi org delete <id>');
105
+ requireArg(id, 'id', 'myapi org delete <id>');
113
106
  const config = requireConfig();
114
107
  await hq.deleteOrg(config.api_key, id);
115
108
  success(`Org ${id} deleted`);
@@ -122,28 +115,18 @@ export async function importOrg(args, flags) {
122
115
  error('Missing required arguments.\nUsage: myapi org sync-brand <domain> [--org <id>]\n(Or set default: myapi config set-domain <domain>)');
123
116
  const result = await hq.importOrg(config.api_key, orgId, domain);
124
117
  const importId = result.job_id;
125
- process.stdout.write('Importing ');
126
- const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
127
- let i = 0;
128
- let elapsed = 0;
129
- while (elapsed < SYNC_BRAND_TIMEOUT_MS) {
130
- const status = await hq.getOrgImportStatus(config.api_key, importId);
131
- if (status.status === 'awaiting_confirm') {
132
- process.stdout.write('\r\x1b[K');
133
- const org = await hq.confirmOrgImport(config.api_key, importId);
134
- success(`Import complete! Org ID: ${org.id}`);
135
- return;
136
- }
137
- if (status.status === 'failed') {
138
- process.stdout.write('\r\x1b[K');
139
- error('Import failed');
140
- }
141
- process.stdout.write(`\rImporting ${chars[i++ % chars.length]}`);
142
- await sleep(3000);
143
- elapsed += 3000;
144
- }
145
- process.stdout.write('\r\x1b[K');
146
- error(`Import timed out after ${SYNC_BRAND_TIMEOUT_MS / 1000}s. The job may still complete in the background — re-run "myapi org sync-brand" to retry.`);
118
+ await pollJob({
119
+ label: 'Importing',
120
+ timeoutMs: SYNC_BRAND_TIMEOUT_MS,
121
+ intervalMs: 3000,
122
+ check: () => hq.getOrgImportStatus(config.api_key, importId),
123
+ isDone: s => s.status === 'awaiting_confirm',
124
+ isFailed: s => s.status === 'failed',
125
+ failedMessage: 'Import failed',
126
+ timeoutMessage: `Import timed out after ${SYNC_BRAND_TIMEOUT_MS / 1000}s. The job may still complete in the background — re-run "myapi org sync-brand" to retry.`,
127
+ });
128
+ const org = await hq.confirmOrgImport(config.api_key, importId);
129
+ success(`Import complete! Org ID: ${org.id}`);
147
130
  }
148
131
  // ── Dispatcher ───────────────────────────────────────────────────────────────
149
132
  const SUBCOMMAND_USAGE = {
@@ -1,19 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
- import * as readline from 'readline';
5
4
  import { loadConfig, saveConfig, addAccount, loadFullConfig } from '../config.js';
6
5
  import { info, success } from '../output.js';
6
+ import { ask, confirm } from '../prompt.js';
7
7
  import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
8
- function ask(rl, q) {
9
- return new Promise(resolve => rl.question(q, resolve));
10
- }
11
- function yn(answer, defaultYes = true) {
12
- const t = answer.trim().toLowerCase();
13
- if (t === '')
14
- return defaultYes;
15
- return t === 'y' || t === 'yes';
16
- }
17
8
  // ---------------------------------------------------------------------------
18
9
  // Skills installation
19
10
  // ---------------------------------------------------------------------------
@@ -53,10 +44,15 @@ export async function installSkills() {
53
44
  for (const skill of skills) {
54
45
  const link = path.join(dir, skill);
55
46
  const target = path.join(SKILLS_CANONICAL, skill);
47
+ // Only remove if it's already a symlink we own. Avoids clobbering a
48
+ // user-authored skill that happens to share the same `my-X-api/`
49
+ // name. ENOENT here just means "nothing to clean up".
56
50
  try {
57
- fs.rmSync(link, { recursive: true, force: true });
51
+ const stat = fs.lstatSync(link);
52
+ if (stat.isSymbolicLink())
53
+ fs.rmSync(link, { force: true });
58
54
  }
59
- catch { /* ignore */ }
55
+ catch { /* ENOENT: nothing there */ }
60
56
  fs.symlinkSync(target, link);
61
57
  }
62
58
  info(` ✓ ${agent}`);
@@ -74,13 +70,48 @@ export async function installSkills() {
74
70
  }
75
71
  }
76
72
  // ---------------------------------------------------------------------------
73
+ // Shared helpers
74
+ // ---------------------------------------------------------------------------
75
+ // Resolve the user's "install skills?" preference: explicit flag wins, else
76
+ // prompt (defaulting to yes). `--yes` short-circuits to true.
77
+ async function resolveSkillsPreference(flags) {
78
+ if (flags['install-skills'])
79
+ return true;
80
+ if (flags['no-skills'])
81
+ return false;
82
+ if (flags.yes)
83
+ return true;
84
+ return confirm('› Install the MyAPI skills pack? (Y/n) ', true);
85
+ }
86
+ // Resolve default org and funnel for a key by inspecting the account's orgs.
87
+ // Used by both `setup` and `import-key`. Picks the most recently-created
88
+ // org/funnel as the default; preserves the caller's hint if it still exists.
89
+ async function resolveDefaults(apiKey, hint = {}) {
90
+ let org = hint.org ?? '';
91
+ let funnel = hint.funnel ?? '';
92
+ try {
93
+ const orgs = await hq.listOrgs(apiKey);
94
+ if (orgs.length > 0) {
95
+ if (!org || !orgs.find(o => o.id === org)) {
96
+ org = orgs[orgs.length - 1].id;
97
+ }
98
+ const funnels = await sdkFunnel.listFunnels(apiKey, org);
99
+ if (funnels.length > 0 && (!funnel || !funnels.find((f) => f.id === funnel))) {
100
+ funnel = funnels[funnels.length - 1].id;
101
+ }
102
+ }
103
+ }
104
+ catch { /* non-fatal */ }
105
+ return { org, funnel };
106
+ }
107
+ // ---------------------------------------------------------------------------
77
108
  // Registered flow
78
109
  // ---------------------------------------------------------------------------
79
- async function registeredFlow(rl) {
80
- const email = (await ask(rl, '› Email? ')).trim();
110
+ async function registeredFlow() {
111
+ const email = await ask('› Email? ');
81
112
  await hq.sendCode(email);
82
113
  info(`› Sent a code to ${email} · paste it below`);
83
- const code = (await ask(rl, '› Code? ')).trim();
114
+ const code = await ask('› Code? ');
84
115
  const data = await hq.verifyCode(email, code);
85
116
  return { ...data, email };
86
117
  }
@@ -90,16 +121,39 @@ async function anonymousFlow() {
90
121
  // ---------------------------------------------------------------------------
91
122
  // Main setup command
92
123
  // ---------------------------------------------------------------------------
124
+ const IMPORT_KEY_HELP = `Usage: myapi auth import-key <api_key> [--install-skills] [--no-skills]
125
+
126
+ Imports an existing API key non-interactively. Use this in CI, Docker, or any
127
+ environment where the interactive "myapi auth setup" flow is not practical.
128
+
129
+ The key is validated against the API before being saved. Your default org and
130
+ funnel are auto-detected from the account and written to the local config.
131
+
132
+ Flags:
133
+ --install-skills Also install the MyAPI skills pack for AI agents after importing
134
+ --no-skills Skip skills installation even if previously installed
135
+
136
+ Examples:
137
+ myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx
138
+ myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx --install-skills`;
139
+ const SETUP_HELP = `Usage: myapi auth setup [--anonymous] [--yes] [--install-skills|--no-skills]
140
+
141
+ Configures your account and stores your default org and funnel so you don't
142
+ need to pass --org or --funnel on every command.
143
+
144
+ Flags:
145
+ --anonymous Skip registration, create anonymous account
146
+ --yes Skip confirmation prompts
147
+ --install-skills Auto-install skills pack
148
+ --no-skills Skip skills installation`;
93
149
  // myapi auth import-key <key> — non-interactively import a raw API key.
94
150
  export async function importKey(apiKey, flags) {
95
151
  if (flags.help || !apiKey) {
96
- info('Usage: myapi auth import-key <api_key> [--install-skills] [--no-skills]\n\nImports an existing API key non-interactively. Use this in CI, Docker, or any\nenvironment where the interactive "myapi auth setup" flow is not practical.\n\nThe key is validated against the API before being saved. Your default org and\nfunnel are auto-detected from the account and written to the local config.\n\nFlags:\n --install-skills Also install the MyAPI skills pack for AI agents after importing\n --no-skills Skip skills installation even if previously installed\n\nExamples:\n myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx\n myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx --install-skills');
152
+ info(IMPORT_KEY_HELP);
97
153
  return;
98
154
  }
99
155
  let accountId = '';
100
156
  let email;
101
- let defaultOrg = '';
102
- let defaultFunnel = '';
103
157
  try {
104
158
  const me = await hq.getAccount(apiKey);
105
159
  accountId = me.account_id ?? '';
@@ -108,25 +162,26 @@ export async function importKey(apiKey, flags) {
108
162
  catch (e) {
109
163
  throw new Error(`Could not verify API key: ${e.message}`);
110
164
  }
111
- try {
112
- const orgs = await hq.listOrgs(apiKey);
113
- if (orgs.length > 0) {
114
- defaultOrg = orgs[orgs.length - 1].id;
115
- const funnels = await sdkFunnel.listFunnels(apiKey, defaultOrg);
116
- if (funnels.length > 0)
117
- defaultFunnel = funnels[funnels.length - 1].id;
118
- }
119
- }
120
- catch { /* non-fatal */ }
121
- const wantsSkills = flags['install-skills'] ? true : flags['no-skills'] ? false : true;
122
- addAccount({ api_key: apiKey, account_id: accountId, email, default_org: defaultOrg, default_funnel: defaultFunnel, skills_installed: wantsSkills });
165
+ const { org: defaultOrg, funnel: defaultFunnel } = await resolveDefaults(apiKey);
166
+ const wantsSkills = flags['no-skills'] ? false : true;
167
+ addAccount({
168
+ api_key: apiKey, account_id: accountId, email,
169
+ default_org: defaultOrg, default_funnel: defaultFunnel,
170
+ skills_installed: wantsSkills,
171
+ });
123
172
  success(`› ✓ Key imported${email ? ` · ${email}` : ''}`);
124
173
  if (wantsSkills)
125
174
  await installSkills();
126
175
  }
176
+ // Persist a new account + (optionally) install skills.
177
+ async function installAndPersistSkills(account, wantsSkills) {
178
+ addAccount({ ...account, skills_installed: wantsSkills });
179
+ if (wantsSkills)
180
+ await installSkills();
181
+ }
127
182
  export async function setup(flags = {}) {
128
183
  if (flags.help) {
129
- info('Usage: myapi auth setup [--anonymous] [--yes] [--install-skills|--no-skills]\n\nConfigures your account and stores your default org and funnel so you don\'t need to pass --org or --funnel on every command.\n\nFlags:\n --anonymous Skip registration, create anonymous account\n --yes Skip confirmation prompts\n --install-skills Auto-install skills pack\n --no-skills Skip skills installation');
184
+ info(SETUP_HELP);
130
185
  return;
131
186
  }
132
187
  info('› Configuring MyAPI…');
@@ -139,27 +194,20 @@ export async function setup(flags = {}) {
139
194
  const activeType = existing.is_anonymous ? 'anonymous' : 'registered';
140
195
  const othersNote = total > 1 ? ` · ${total - 1} more saved` : '';
141
196
  info(`› Connected: ${activeLabel} (${activeType})${othersNote}`);
142
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
- const add = await ask(rl, '› Connect a new account? (y/N) ');
144
- if (!yn(add, false)) {
197
+ const add = await confirm('› Connect a new account? (y/N) ', false);
198
+ if (!add) {
145
199
  if (!existing.skills_installed) {
146
- const ans = await ask(rl, '› Install the MyAPI skills pack? (Y/n) ');
147
- rl.close();
148
- if (yn(ans)) {
200
+ const wants = await resolveSkillsPreference(flags);
201
+ if (wants) {
149
202
  await installSkills();
150
203
  success('› Skills installed.');
151
204
  saveConfig({ ...existing, skills_installed: true });
152
205
  }
153
206
  }
154
- else {
155
- rl.close();
156
- }
157
207
  return;
158
208
  }
159
- rl.close();
160
209
  // Fall through to setup flow — new account will be added to the list.
161
210
  }
162
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
163
211
  let apiKey = '';
164
212
  let accountId = '';
165
213
  let defaultOrg = '';
@@ -167,82 +215,62 @@ export async function setup(flags = {}) {
167
215
  let subdomainUrl = '';
168
216
  let isAnonymous = false;
169
217
  let email = '';
170
- // Determine skills preference from flags before any prompts.
171
- const skillsFromFlag = flags['install-skills'] ? true : flags['no-skills'] ? false : null;
218
+ const useAnon = flags.anonymous || flags.anon;
219
+ const wantsRegister = useAnon ? false : await confirm('› Register with email? (Y/n, or N to continue anonymously) ', true);
220
+ if (wantsRegister) {
221
+ const data = await registeredFlow();
222
+ apiKey = data.api_key;
223
+ accountId = data.account_id;
224
+ defaultOrg = data.default_org;
225
+ defaultFunnel = data.default_funnel;
226
+ email = data.email;
227
+ }
228
+ else {
229
+ info('› Continuing as anonymous — sites ship to *.makeautonomous.com');
230
+ const data = await anonymousFlow();
231
+ apiKey = data.api_key;
232
+ accountId = data.account_id;
233
+ defaultOrg = data.default_org;
234
+ defaultFunnel = data.default_funnel;
235
+ subdomainUrl = data.subdomain_url;
236
+ isAnonymous = true;
237
+ }
238
+ const wantsSkills = await resolveSkillsPreference(flags);
239
+ await installAndPersistSkills({
240
+ api_key: apiKey,
241
+ account_id: accountId,
242
+ email: email || undefined,
243
+ default_org: defaultOrg,
244
+ default_funnel: defaultFunnel,
245
+ is_anonymous: isAnonymous,
246
+ }, wantsSkills);
247
+ success(`› ✓ saved to ~/.myapi/config.json`);
248
+ // Validate key and ensure org/funnel defaults are still correct.
249
+ // (resolveDefaults is non-fatal if the API call fails.)
172
250
  try {
173
- const useAnon = flags.anonymous || flags.anon;
174
- const createAns = useAnon ? 'n' : await ask(rl, '› Register with email? (Y/n, or N to continue anonymously) ');
175
- if (yn(createAns)) {
176
- const data = await registeredFlow(rl);
177
- apiKey = data.api_key;
178
- accountId = data.account_id;
179
- defaultOrg = data.default_org;
180
- defaultFunnel = data.default_funnel;
181
- email = data.email;
182
- }
183
- else {
184
- info('› Continuing as anonymous — sites ship to *.makeautonomous.com');
185
- const data = await anonymousFlow();
186
- apiKey = data.api_key;
187
- accountId = data.account_id;
188
- defaultOrg = data.default_org;
189
- defaultFunnel = data.default_funnel;
190
- subdomainUrl = data.subdomain_url;
191
- isAnonymous = true;
192
- }
193
- const wantsSkills = skillsFromFlag !== null ? skillsFromFlag : flags.yes ? true : yn(await ask(rl, '› Install the MyAPI skills pack? (Y/n) '));
194
- addAccount({
195
- api_key: apiKey,
196
- account_id: accountId,
197
- email: email || undefined,
198
- default_org: defaultOrg,
199
- default_funnel: defaultFunnel,
200
- is_anonymous: isAnonymous,
201
- skills_installed: wantsSkills,
202
- });
203
- success(`› ✓ saved to ~/.myapi/config.json`);
204
- if (wantsSkills) {
205
- await installSkills();
206
- }
207
- // Validate key and ensure org/funnel defaults are correct.
208
- try {
209
- await hq.getAccount(apiKey);
210
- let resolvedOrg = defaultOrg;
211
- let resolvedFunnel = defaultFunnel;
212
- const orgs = await hq.listOrgs(apiKey);
213
- if (orgs.length > 0) {
214
- const latest = orgs[orgs.length - 1];
215
- if (!resolvedOrg || !orgs.find(o => o.id === resolvedOrg)) {
216
- resolvedOrg = latest.id;
217
- info(`› Auto-selected org: ${latest.name} (${resolvedOrg})`);
218
- }
219
- const funnels = await sdkFunnel.listFunnels(apiKey, resolvedOrg);
220
- if (funnels.length > 0 && (!resolvedFunnel || !funnels.find((f) => f.id === resolvedFunnel))) {
221
- resolvedFunnel = funnels[funnels.length - 1].id;
222
- info(`› Auto-selected funnel: ${resolvedFunnel}`);
223
- }
224
- if (resolvedOrg !== defaultOrg || resolvedFunnel !== defaultFunnel) {
225
- const current = loadConfig();
226
- if (current) {
227
- saveConfig({ ...current, default_org: resolvedOrg, default_funnel: resolvedFunnel });
228
- defaultOrg = resolvedOrg;
229
- defaultFunnel = resolvedFunnel;
230
- }
231
- }
251
+ await hq.getAccount(apiKey);
252
+ const resolved = await resolveDefaults(apiKey, { org: defaultOrg, funnel: defaultFunnel });
253
+ if (resolved.org !== defaultOrg || resolved.funnel !== defaultFunnel) {
254
+ const current = loadConfig();
255
+ if (current) {
256
+ saveConfig({ ...current, default_org: resolved.org, default_funnel: resolved.funnel });
257
+ if (resolved.org !== defaultOrg)
258
+ info(`› Auto-selected org: ${resolved.org}`);
259
+ if (resolved.funnel !== defaultFunnel)
260
+ info(`› Auto-selected funnel: ${resolved.funnel}`);
261
+ defaultOrg = resolved.org;
262
+ defaultFunnel = resolved.funnel;
232
263
  }
233
264
  }
234
- catch { /* non-fatal */ }
235
- if (isAnonymous) {
236
- success('› Setup complete. No card, no email, ready to ship.');
237
- if (subdomainUrl)
238
- info(`› Your funnel: ${subdomainUrl}`);
239
- info('› Upgrade anytime — myapi auth link');
240
- }
241
- else {
242
- success(`› Setup complete. Org ${defaultOrg} ready · $5.00 free credits added.`);
243
- }
244
265
  }
245
- finally {
246
- rl.close();
266
+ catch { /* non-fatal */ }
267
+ if (isAnonymous) {
268
+ success('› Setup complete. No card, no email, ready to ship.');
269
+ if (subdomainUrl)
270
+ info(`› Your funnel: ${subdomainUrl}`);
271
+ info('› Upgrade anytime — myapi auth link');
272
+ }
273
+ else {
274
+ success(`› Setup complete. Org ${defaultOrg} ready · $5.00 free credits added.`);
247
275
  }
248
276
  }
@@ -126,6 +126,11 @@ export async function latestVersion() {
126
126
  return null;
127
127
  }
128
128
  }
129
+ // Stable-only semver comparison (1.2.3). Prerelease versions (1.2.3-wip.0)
130
+ // produce NaN in the patch slot, and `n > NaN` is always false — which is
131
+ // what we want: a prerelease user should not be auto-rolled back to stable.
132
+ // Auto-update is opt-in via the `latest` dist-tag (always stable); see
133
+ // checkForUpdate() above.
129
134
  export function isNewer(latest, current) {
130
135
  const toNum = (v) => v.split('.').map(Number);
131
136
  const [lMaj, lMin, lPat] = toNum(latest);
@@ -2,7 +2,7 @@ import type { FlagSchema } from '../flags.js';
2
2
  import { type Flags } from '../helpers.js';
3
3
  export declare const SCHEMA: FlagSchema;
4
4
  export declare function list(flags: Flags): Promise<void>;
5
- export declare function create(flags: Flags): Promise<void>;
5
+ export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
6
6
  export declare function del(id: string, flags: Flags): Promise<void>;
7
7
  export declare function delivery(id: string, flags: Flags): Promise<void>;
8
8
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -20,12 +20,12 @@ export async function list(flags) {
20
20
  empty: 'No webhook endpoints yet. Create one with: myapi webhook create --name <name>',
21
21
  });
22
22
  }
23
- export async function create(flags) {
23
+ export async function create(nameArg, flags) {
24
24
  const config = requireConfig();
25
- const orgId = requireOrg(flags, config, 'myapi webhook create --name <name> [--description <desc>] [--org <id>]');
26
- const name = flags.name;
25
+ const orgId = requireOrg(flags, config, 'myapi webhook create <name> [--description <desc>] [--org <id>]');
26
+ const name = nameArg || flags.name;
27
27
  if (!name)
28
- error('Missing required arguments.\nUsage: myapi webhook create --name <name> [--description <desc>] [--org <id>]');
28
+ error('Missing required arguments.\nUsage: myapi webhook create <name> [--description <desc>] [--org <id>]\n or: myapi webhook create --name <name> [--description <desc>] [--org <id>]');
29
29
  const description = flags.description;
30
30
  const res = await sdkWebhook.createEndpoint(config.api_key, orgId, name, description);
31
31
  success(`Webhook created! ID: ${res.id}\nInbound URL: ${res.url}`);
@@ -49,7 +49,10 @@ export async function delivery(id, flags) {
49
49
  // ── Dispatcher ───────────────────────────────────────────────────────────────
50
50
  const SUBCOMMAND_USAGE = {
51
51
  'list': 'myapi webhook list [--org <id>] [--json]',
52
- 'create': 'myapi webhook create --name <name> [--description <desc>] [--org <id>]',
52
+ 'create': `myapi webhook create <name> [--description <desc>] [--org <id>]
53
+ myapi webhook create --name <name> [--description <desc>] [--org <id>]
54
+
55
+ Either form works; the positional name is the recommended shape.`,
53
56
  'delete': 'myapi webhook delete <id> [--org <id>]',
54
57
  'delivery': 'myapi webhook delivery <delivery_id> [--org <id>]',
55
58
  };
@@ -63,7 +66,20 @@ Subcommands:
63
66
  delete Delete a webhook endpoint
64
67
  delivery Inspect a specific webhook delivery (payload, received_at)
65
68
 
66
- All commands accept --org <id> (or set default: myapi config set-org <id>).`);
69
+ All commands accept --org <id> (or set default: myapi config set-org <id>).
70
+
71
+ Payload contract:
72
+ The inbound URL accepts ANY JSON body — there's no enforced schema.
73
+ Whatever you POST is stored verbatim alongside headers + timestamp.
74
+ Workflows reference fields with {{ payload.field }} templating.
75
+
76
+ Example — wire a contact form to email + Slack:
77
+ WID=$(myapi webhook create "contact-form" --json | jq -r .id)
78
+ myapi workflow create "Contact handler" --endpoint-id $WID --steps '[
79
+ {"type":"send_email","to":"{{ payload.email }}","subject":"Thanks!","body":"..."},
80
+ {"type":"slack","webhook_url":"https://hooks.slack.com/...","text":"New: {{ payload.message }}"}
81
+ ]'
82
+ See the my-webhook-api skill for the full HTML + JS recipe.`);
67
83
  return;
68
84
  }
69
85
  if (flags.help) {
@@ -76,7 +92,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
76
92
  }
77
93
  switch (subcommand) {
78
94
  case 'list': return list(flags);
79
- case 'create': return create(flags);
95
+ case 'create': return create(args[0], flags);
80
96
  case 'delete': return del(args[0], flags);
81
97
  case 'delivery': return delivery(args[0], flags);
82
98
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi webhook --help" for a list of valid subcommands.`);
@@ -3,7 +3,7 @@ import { type Flags } from '../helpers.js';
3
3
  export declare const SCHEMA: FlagSchema;
4
4
  export declare function list(flags: Flags): Promise<void>;
5
5
  export declare function get(id: string, flags: Flags): Promise<void>;
6
- export declare function create(flags: Flags): Promise<void>;
6
+ export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
7
7
  export declare function update(id: string, flags: Flags): Promise<void>;
8
8
  export declare function enable(id: string, flags: Flags): Promise<void>;
9
9
  export declare function disable(id: string, flags: Flags): Promise<void>;
@@ -9,6 +9,23 @@ export const SCHEMA = {
9
9
  steps: 'string',
10
10
  'no-enable': 'boolean',
11
11
  };
12
+ // Mirrors the backend's SupportedStepTypes list. Both alias and underscore
13
+ // forms are accepted by the workflow runner. Keep this in sync if the
14
+ // backend grows new step types.
15
+ const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack'];
16
+ function validateSteps(steps) {
17
+ if (!Array.isArray(steps))
18
+ error('--steps must be a JSON array of step objects.');
19
+ steps.forEach((s, i) => {
20
+ if (!s || typeof s !== 'object')
21
+ error(`step ${i}: must be a JSON object.`);
22
+ if (!s.type)
23
+ error(`step ${i}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
24
+ if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
25
+ error(`step ${i}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
26
+ }
27
+ });
28
+ }
12
29
  function summarizeWorkflow(w) {
13
30
  return {
14
31
  id: w.id,
@@ -52,13 +69,13 @@ export async function get(id, flags) {
52
69
  const wf = await sdkWorkflow.getWorkflow(config.api_key, orgId, id);
53
70
  printJson(wf);
54
71
  }
55
- export async function create(flags) {
72
+ export async function create(nameArg, flags) {
56
73
  const config = requireConfig();
57
- const orgId = requireOrg(flags, config, 'myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
58
- const name = flags.name;
74
+ const orgId = requireOrg(flags, config, 'myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
75
+ const name = nameArg || flags.name;
59
76
  const endpointId = flags['endpoint-id'];
60
77
  if (!name || !endpointId || !flags.steps) {
61
- error('Missing required arguments.\nUsage: myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
78
+ error('Missing required arguments.\nUsage: myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]\n or: myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
62
79
  }
63
80
  let steps;
64
81
  try {
@@ -67,6 +84,7 @@ export async function create(flags) {
67
84
  catch {
68
85
  error('Invalid JSON for --steps');
69
86
  }
87
+ validateSteps(steps);
70
88
  const wf = await sdkWorkflow.createWorkflow(config.api_key, orgId, {
71
89
  name,
72
90
  trigger_config: { endpoint_id: endpointId },
@@ -98,6 +116,7 @@ export async function update(id, flags) {
98
116
  catch {
99
117
  error('Invalid JSON for --steps');
100
118
  }
119
+ validateSteps(payload.steps);
101
120
  }
102
121
  if (!payload.name && !payload.trigger_config && !payload.steps) {
103
122
  error('Nothing to update. Provide at least one of --name, --endpoint-id, --steps.');
@@ -156,7 +175,27 @@ export async function getRun(runId, flags) {
156
175
  const SUBCOMMAND_USAGE = {
157
176
  'list': 'myapi workflow list [--org <id>] [--json]',
158
177
  'get': 'myapi workflow get <id> [--org <id>]',
159
- 'create': 'myapi workflow create --name <str> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]',
178
+ 'create': `myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]
179
+ myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]
180
+
181
+ Either form works; the positional name is the recommended shape.
182
+
183
+ --steps is a JSON array of step objects. Supported step types:
184
+ send_email | email — fields: from, to, subject, body | template_id, template_vars
185
+ slack_message | slack — fields: webhook_url, text
186
+
187
+ Either alias works (e.g. type: "email" and type: "send_email" both run
188
+ the same step). Field values support {{ payload.field }} templating to
189
+ reference the incoming webhook payload, e.g. "to": "{{ payload.email }}".
190
+ Unknown step types are rejected at create time, not at execute time.
191
+
192
+ Example — fire on every webhook POST, send a thank-you email:
193
+ myapi workflow create "Contact handler" --endpoint-id <wid> --steps '[
194
+ {"type":"send_email","from":"hello@x.com","to":"{{ payload.email }}",
195
+ "subject":"Thanks!","body":"We got your message."}
196
+ ]'
197
+
198
+ See the my-workflow-api skill for the full form-to-email recipe.`,
160
199
  'update': 'myapi workflow update <id> [--name <str>] [--endpoint-id <id>] [--steps <json>] [--org <id>]',
161
200
  'enable': 'myapi workflow enable <id> [--org <id>]',
162
201
  'disable': 'myapi workflow disable <id> [--org <id>]',
@@ -197,7 +236,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
197
236
  switch (subcommand) {
198
237
  case 'list': return list(flags);
199
238
  case 'get': return get(args[0], flags);
200
- case 'create': return create(flags);
239
+ case 'create': return create(args[0], flags);
201
240
  case 'update': return update(args[0], flags);
202
241
  case 'enable': return enable(args[0], flags);
203
242
  case 'disable': return disable(args[0], flags);
package/dist/config.js CHANGED
@@ -48,8 +48,14 @@ export function loadConfig() {
48
48
  config.api_key = envKey;
49
49
  return config;
50
50
  }
51
- export function saveConfig(config) {
51
+ // Single point of truth for writes. Ensures CONFIG_DIR exists and that the
52
+ // file is created with mode 0o600 (owner read/write only) — no caller has
53
+ // to remember either.
54
+ function writeFullConfig(full) {
52
55
  ensureDir();
56
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
57
+ }
58
+ export function saveConfig(config) {
53
59
  const full = loadFullConfig() ?? { active: 0, accounts: [] };
54
60
  const { autocomplete_setup, ...account } = config;
55
61
  if (full.accounts.length === 0)
@@ -58,10 +64,9 @@ export function saveConfig(config) {
58
64
  full.accounts[full.active] = account;
59
65
  if (autocomplete_setup !== undefined)
60
66
  full.autocomplete_setup = autocomplete_setup;
61
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
67
+ writeFullConfig(full);
62
68
  }
63
69
  export function addAccount(account) {
64
- ensureDir();
65
70
  const full = loadFullConfig() ?? { active: 0, accounts: [] };
66
71
  // Replace if same account_id already exists.
67
72
  const existing = full.accounts.findIndex(a => a.account_id === account.account_id);
@@ -73,7 +78,7 @@ export function addAccount(account) {
73
78
  full.accounts.push(account);
74
79
  full.active = full.accounts.length - 1;
75
80
  }
76
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
81
+ writeFullConfig(full);
77
82
  return full.active;
78
83
  }
79
84
  export function switchAccount(index) {
@@ -81,7 +86,7 @@ export function switchAccount(index) {
81
86
  if (!full || index < 0 || index >= full.accounts.length)
82
87
  return false;
83
88
  full.active = index;
84
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
89
+ writeFullConfig(full);
85
90
  return true;
86
91
  }
87
92
  export function listAccounts() {