@myapihq/cli 1.2.8 → 1.3.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 (36) hide show
  1. package/dist/commands/email/index.js +1 -0
  2. package/dist/commands/email/mailbox.js +28 -1
  3. package/dist/commands/email/verify.d.ts +2 -2
  4. package/dist/commands/email/verify.js +85 -16
  5. package/dist/commands/git.d.ts +22 -0
  6. package/dist/commands/git.js +367 -0
  7. package/dist/commands/pixel.js +6 -3
  8. package/dist/commands/queue-validation.test.d.ts +1 -0
  9. package/dist/commands/queue-validation.test.js +38 -0
  10. package/dist/commands/queue.d.ts +14 -0
  11. package/dist/commands/queue.js +215 -0
  12. package/dist/commands/task-validation.test.d.ts +1 -0
  13. package/dist/commands/task-validation.test.js +37 -0
  14. package/dist/commands/task.d.ts +18 -0
  15. package/dist/commands/task.js +288 -0
  16. package/dist/commands/workflow-validation.test.js +27 -0
  17. package/dist/commands/workflow.js +17 -1
  18. package/dist/completion.js +6 -3
  19. package/dist/exposes.test.js +3 -0
  20. package/dist/index.js +21 -0
  21. package/dist/sdk-email-forwarding.test.d.ts +1 -0
  22. package/dist/sdk-email-forwarding.test.js +48 -0
  23. package/dist/sdk-email-verify-bulk.test.d.ts +1 -0
  24. package/dist/sdk-email-verify-bulk.test.js +57 -0
  25. package/dist/sdk-git.test.d.ts +1 -0
  26. package/dist/sdk-git.test.js +115 -0
  27. package/dist/sdk-queue.test.d.ts +1 -0
  28. package/dist/sdk-queue.test.js +86 -0
  29. package/dist/sdk-task.test.d.ts +1 -0
  30. package/dist/sdk-task.test.js +110 -0
  31. package/dist/skills/my-email-api/README.md +45 -0
  32. package/dist/skills/my-email-api/SKILL.md +80 -0
  33. package/dist/skills/my-email-api/claude/.claude-plugin/plugin.json +6 -0
  34. package/dist/skills/my-email-api/openapi/.gitkeep +0 -0
  35. package/dist/skills/my-workflow-api/SKILL.md +10 -1
  36. package/package.json +2 -2
@@ -26,7 +26,9 @@ export const SCHEMA = {
26
26
  //
27
27
  // 2026-05-15: backend added `http_request` (alias `http`). See
28
28
  // myapi-hq/internal/routes/workflow/execute.go.
29
- const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack', 'http_request', 'http'];
29
+ // 2026-05-19: `enqueue_job` (alias `enqueue`) added hands durable work to
30
+ // my-queue-api. Backend step pending; see the orchestration cross-repo prompt.
31
+ const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack', 'http_request', 'http', 'enqueue_job', 'enqueue'];
30
32
  const HTTP_METHODS = new Set(['GET', 'POST', 'PATCH', 'PUT', 'DELETE']);
31
33
  const SLACK_HOOK_RE = /^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+/;
32
34
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -110,6 +112,20 @@ export function _validateSteps(steps) {
110
112
  }
111
113
  }
112
114
  }
115
+ if (s.type === 'enqueue_job' || s.type === 'enqueue') {
116
+ if (!s.queue || typeof s.queue !== 'string') {
117
+ return `${where} (${s.type}): missing required field "queue" (the target queue name).`;
118
+ }
119
+ if (s.payload !== undefined && typeof s.payload !== 'string') {
120
+ return `${where} (${s.type}): "payload" must be a string (template substitutions supported).`;
121
+ }
122
+ if (s.dedup_key !== undefined && typeof s.dedup_key !== 'string') {
123
+ return `${where} (${s.type}): "dedup_key" must be a string.`;
124
+ }
125
+ if (s.delay_seconds !== undefined && typeof s.delay_seconds !== 'number') {
126
+ return `${where} (${s.type}): "delay_seconds" must be a number.`;
127
+ }
128
+ }
113
129
  }
114
130
  return null;
115
131
  }
@@ -27,9 +27,9 @@ const BLOCK_END = `# end ${PROGRAM} completion`;
27
27
  // is missing here.
28
28
  export const COMMANDS = [
29
29
  'audience', 'auth', 'billing', 'company', 'completion', 'config', 'container',
30
- 'crm', 'database', 'domain', 'email', 'fn', 'funnel', 'help', 'image',
31
- 'install-skills', 'keys', 'llm', 'org', 'payments', 'people', 'pixel',
32
- 'setup', 'status', 'storage', 'update', 'url', 'webhook', 'whoami',
30
+ 'crm', 'database', 'domain', 'email', 'fn', 'funnel', 'git', 'help', 'image',
31
+ 'install-skills', 'keys', 'llm', 'org', 'payments', 'people', 'pixel', 'queue',
32
+ 'setup', 'status', 'storage', 'task', 'update', 'url', 'webhook', 'whoami',
33
33
  'workflow',
34
34
  ];
35
35
  // command → subcommands, for `myapi <command> <TAB>`. Mirrors each
@@ -58,6 +58,9 @@ export const SUBCOMMANDS = {
58
58
  fn: ['create', 'deploy', 'env', 'runs', 'list', 'get', 'delete'],
59
59
  payments: ['connect', 'status', 'charge', 'list', 'get', 'refund'],
60
60
  container: ['create', 'deploy', 'list', 'get', 'logs', 'delete'],
61
+ git: ['create', 'list', 'get', 'delete', 'refs', 'log', 'show', 'tree', 'blob', 'diff', 'commit', 'create-branch', 'delete-branch', 'tag', 'merge', 'repack'],
62
+ queue: ['create', 'list', 'get', 'enqueue', 'jobs', 'job'],
63
+ task: ['create', 'list', 'get', 'claim', 'extend', 'resolve', 'fail', 'cancel'],
61
64
  completion: ['install', 'uninstall'],
62
65
  };
63
66
  // ── Completion request handling ──────────────────────────────────────────────
@@ -40,6 +40,9 @@ const COMMAND_MODULES = [
40
40
  './commands/fn.js',
41
41
  './commands/payments.js',
42
42
  './commands/container.js',
43
+ './commands/git.js',
44
+ './commands/queue.js',
45
+ './commands/task.js',
43
46
  ];
44
47
  const ENDPOINT_PATTERN = /^(GET|POST|PATCH|PUT|DELETE) \/[A-Za-z0-9_\-./{}]*$/;
45
48
  describe('every CLI command exports a typed EXPOSES array (S-101)', () => {
package/dist/index.js CHANGED
@@ -32,6 +32,9 @@ import * as crmCmd from './commands/crm/index.js';
32
32
  import * as fnCmd from './commands/fn.js';
33
33
  import * as paymentsCmd from './commands/payments.js';
34
34
  import * as containerCmd from './commands/container.js';
35
+ import * as gitCmd from './commands/git.js';
36
+ import * as queueCmd from './commands/queue.js';
37
+ import * as taskCmd from './commands/task.js';
35
38
  // Each command file declares the value flags it understands. We union them
36
39
  // into a single schema for the upfront parse, so adding a new value flag in
37
40
  // one command means editing one file (its SCHEMA), not a global allowlist.
@@ -60,6 +63,9 @@ const COMBINED_SCHEMA = {
60
63
  ...fnCmd.SCHEMA,
61
64
  ...paymentsCmd.SCHEMA,
62
65
  ...containerCmd.SCHEMA,
66
+ ...gitCmd.SCHEMA,
67
+ ...queueCmd.SCHEMA,
68
+ ...taskCmd.SCHEMA,
63
69
  // Top-level flags
64
70
  version: 'boolean',
65
71
  V: 'boolean',
@@ -219,6 +225,15 @@ async function main() {
219
225
  case 'container':
220
226
  await containerCmd.run(subcommand, restArgs, flags);
221
227
  break;
228
+ case 'git':
229
+ await gitCmd.run(subcommand, restArgs, flags);
230
+ break;
231
+ case 'queue':
232
+ await queueCmd.run(subcommand, restArgs, flags);
233
+ break;
234
+ case 'task':
235
+ await taskCmd.run(subcommand, restArgs, flags);
236
+ break;
222
237
  // Convenience aliases
223
238
  case 'setup':
224
239
  await setupCmd.setup(flags);
@@ -342,6 +357,9 @@ const HELP_TARGETS = {
342
357
  fn: f => fnCmd.run(undefined, [], f),
343
358
  payments: f => paymentsCmd.run(undefined, [], f),
344
359
  container: f => containerCmd.run(undefined, [], f),
360
+ git: f => gitCmd.run(undefined, [], f),
361
+ queue: f => queueCmd.run(undefined, [], f),
362
+ task: f => taskCmd.run(undefined, [], f),
345
363
  org: f => orgCmd.run(undefined, [], f),
346
364
  billing: f => billingCmd.run(undefined, [], f),
347
365
  keys: f => keysCmd.run(undefined, [], f),
@@ -385,6 +403,9 @@ Commands:
385
403
  funnel Manage websites (publish pages, custom domains, funnels)
386
404
  fn Create and deploy functions on the edge runtime
387
405
  container Run containers — services, workers, and scheduled jobs
406
+ git Hosted git repositories — repos, commits, branches, history
407
+ queue Durable job queue — enqueue work, retried against an HTTP consumer
408
+ task Agent-task queue — file, claim, and resolve units of work
388
409
  payments Take payments with Stripe Checkout (connect, charge, refund)
389
410
  webhook Manage inbound webhook endpoints and inspect deliveries
390
411
  email Manage mailboxes, send/read email, templates, and campaigns
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ // SDK-level unit tests for mailbox forwarding —
2
+ // email.setForwarding + email.deleteForwarding. Mocks fetch — no network.
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
4
+ import { email } from '@myapihq/sdk';
5
+ const API_KEY = 'myapi_test_abc';
6
+ let fetchMock;
7
+ function ok(data, status = 200) {
8
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
9
+ status, headers: { 'content-type': 'application/json' },
10
+ });
11
+ }
12
+ function fail(code, status) {
13
+ return new Response(JSON.stringify({ success: false, error: { code, message: code }, meta: {} }), {
14
+ status, headers: { 'content-type': 'application/json' },
15
+ });
16
+ }
17
+ beforeEach(() => { fetchMock = vi.fn(); globalThis.fetch = fetchMock; });
18
+ afterEach(() => { vi.restoreAllMocks(); });
19
+ describe('email.setForwarding', () => {
20
+ it('PUTs {forward_to} to the mailbox forwarding endpoint', async () => {
21
+ fetchMock.mockResolvedValueOnce(ok({ address: 'a@x.com', forward_to: 'b@y.com' }));
22
+ const res = await email.setForwarding(API_KEY, 'a@x.com', 'b@y.com');
23
+ const [url, init] = fetchMock.mock.calls[0];
24
+ expect(url).toContain('/email/mailboxes/a%40x.com/forwarding');
25
+ expect(init.method).toBe('PUT');
26
+ expect(JSON.parse(init.body)).toEqual({ forward_to: 'b@y.com' });
27
+ expect(res.forward_to).toBe('b@y.com');
28
+ });
29
+ it('surfaces FORWARD_LOOP (400)', async () => {
30
+ fetchMock.mockResolvedValueOnce(fail('FORWARD_LOOP', 400));
31
+ await expect(email.setForwarding(API_KEY, 'a@x.com', 'a@x.com'))
32
+ .rejects.toMatchObject({ code: 'FORWARD_LOOP', status: 400 });
33
+ });
34
+ });
35
+ describe('email.deleteForwarding', () => {
36
+ it('DELETEs the forwarding endpoint and resolves on 204', async () => {
37
+ fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
38
+ await email.deleteForwarding(API_KEY, 'a@x.com');
39
+ const [url, init] = fetchMock.mock.calls[0];
40
+ expect(url).toContain('/email/mailboxes/a%40x.com/forwarding');
41
+ expect(init.method).toBe('DELETE');
42
+ });
43
+ it('surfaces MAILBOX_NOT_OWNED (403)', async () => {
44
+ fetchMock.mockResolvedValueOnce(fail('MAILBOX_NOT_OWNED', 403));
45
+ await expect(email.deleteForwarding(API_KEY, 'a@x.com'))
46
+ .rejects.toMatchObject({ code: 'MAILBOX_NOT_OWNED', status: 403 });
47
+ });
48
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ // SDK-level unit tests for async bulk email verification —
2
+ // email.verifyBulk + email.getVerifyJob. Mocks global fetch — no network.
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
4
+ import { email } from '@myapihq/sdk';
5
+ const API_KEY = 'myapi_test_abc';
6
+ const ORG = '11111111-1111-4111-8111-111111111111';
7
+ let fetchMock;
8
+ function ok(data) {
9
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
10
+ status: 200, headers: { 'content-type': 'application/json' },
11
+ });
12
+ }
13
+ function fail(code, status) {
14
+ return new Response(JSON.stringify({ success: false, error: { code, message: code }, meta: {} }), {
15
+ status, headers: { 'content-type': 'application/json' },
16
+ });
17
+ }
18
+ beforeEach(() => { fetchMock = vi.fn(); globalThis.fetch = fetchMock; });
19
+ afterEach(() => { vi.restoreAllMocks(); });
20
+ describe('email.verifyBulk', () => {
21
+ it('POSTs {emails} and returns the job', async () => {
22
+ fetchMock.mockResolvedValueOnce(ok({ job_id: 'j1', status: 'pending', total: 2, catch_all: true }));
23
+ const job = await email.verifyBulk(API_KEY, ORG, ['a@x.com', 'b@y.com']);
24
+ const [url, init] = fetchMock.mock.calls[0];
25
+ expect(url).toContain(`/email/orgs/${ORG}/verify-bulk`);
26
+ expect(init.method).toBe('POST');
27
+ expect(JSON.parse(init.body)).toEqual({ emails: ['a@x.com', 'b@y.com'] });
28
+ expect(job.job_id).toBe('j1');
29
+ });
30
+ it('passes catch_all through when set', async () => {
31
+ fetchMock.mockResolvedValueOnce(ok({ job_id: 'j1', status: 'pending', total: 1 }));
32
+ await email.verifyBulk(API_KEY, ORG, ['a@x.com'], false);
33
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ emails: ['a@x.com'], catch_all: false });
34
+ });
35
+ it('surfaces TOO_MANY_EMAILS (422)', async () => {
36
+ fetchMock.mockResolvedValueOnce(fail('TOO_MANY_EMAILS', 422));
37
+ await expect(email.verifyBulk(API_KEY, ORG, ['a@x.com']))
38
+ .rejects.toMatchObject({ code: 'TOO_MANY_EMAILS', status: 422 });
39
+ });
40
+ });
41
+ describe('email.getVerifyJob', () => {
42
+ it('GETs /verify-jobs/{id} and returns status + results', async () => {
43
+ fetchMock.mockResolvedValueOnce(ok({
44
+ job_id: 'j1', status: 'done', total: 1, completed: 1,
45
+ results: [{ email: 'a@x.com', verdict: 'deliverable', confidence: 0.9, source: 'smtp' }],
46
+ }));
47
+ const job = await email.getVerifyJob(API_KEY, ORG, 'j1');
48
+ expect(job.status).toBe('done');
49
+ expect(job.results?.[0].verdict).toBe('deliverable');
50
+ expect(fetchMock.mock.calls[0][0]).toContain(`/email/orgs/${ORG}/verify-jobs/j1`);
51
+ });
52
+ it('surfaces JOB_NOT_FOUND (404)', async () => {
53
+ fetchMock.mockResolvedValueOnce(fail('JOB_NOT_FOUND', 404));
54
+ await expect(email.getVerifyJob(API_KEY, ORG, 'nope'))
55
+ .rejects.toMatchObject({ code: 'JOB_NOT_FOUND', status: 404 });
56
+ });
57
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ // SDK-level unit tests for the git module. Verifies URL/body shape,
2
+ // envelope-unwrapping, path encoding, and error handling against
3
+ // myapi-hq/internal/routes/git/. Mocks global fetch — no network.
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { git } from '@myapihq/sdk';
6
+ const API_KEY = 'myapi_test_abc';
7
+ const ORG = '11111111-1111-4111-8111-111111111111';
8
+ let fetchMock;
9
+ function ok(data, status = 200) {
10
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
11
+ status, headers: { 'content-type': 'application/json' },
12
+ });
13
+ }
14
+ function fail(code, status) {
15
+ return new Response(JSON.stringify({ success: false, error: { code, message: code }, meta: {} }), {
16
+ status, headers: { 'content-type': 'application/json' },
17
+ });
18
+ }
19
+ beforeEach(() => { fetchMock = vi.fn(); globalThis.fetch = fetchMock; });
20
+ afterEach(() => { vi.restoreAllMocks(); });
21
+ describe('git repos', () => {
22
+ it('createRepo POSTs {name, default_branch?}', async () => {
23
+ fetchMock.mockResolvedValueOnce(ok({ name: 'app', default_branch: 'main' }, 201));
24
+ await git.createRepo(API_KEY, ORG, 'app', 'trunk');
25
+ const [url, init] = fetchMock.mock.calls[0];
26
+ expect(url).toContain(`/git/orgs/${ORG}/repos`);
27
+ expect(init.method).toBe('POST');
28
+ expect(JSON.parse(init.body)).toEqual({ name: 'app', default_branch: 'trunk' });
29
+ });
30
+ it('createRepo omits default_branch when not given', async () => {
31
+ fetchMock.mockResolvedValueOnce(ok({ name: 'app', default_branch: 'main' }, 201));
32
+ await git.createRepo(API_KEY, ORG, 'app');
33
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ name: 'app' });
34
+ });
35
+ it('listRepos unwraps the {repos:[...]} envelope to an array', async () => {
36
+ fetchMock.mockResolvedValueOnce(ok({ repos: [{ name: 'a' }, { name: 'b' }] }));
37
+ expect(await git.listRepos(API_KEY, ORG)).toEqual([{ name: 'a' }, { name: 'b' }]);
38
+ });
39
+ it('getRepo GETs /repos/{repo}', async () => {
40
+ fetchMock.mockResolvedValueOnce(ok({ name: 'app', default_branch: 'main', branches: 2, tags: 1 }));
41
+ const r = await git.getRepo(API_KEY, ORG, 'app');
42
+ expect(r.branches).toBe(2);
43
+ expect(fetchMock.mock.calls[0][0]).toContain(`/git/orgs/${ORG}/repos/app`);
44
+ });
45
+ it('deleteRepo DELETEs and surfaces 404', async () => {
46
+ fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
47
+ await git.deleteRepo(API_KEY, ORG, 'app');
48
+ expect(fetchMock.mock.calls[0][1].method).toBe('DELETE');
49
+ fetchMock.mockResolvedValueOnce(fail('repository not found', 404));
50
+ await expect(git.deleteRepo(API_KEY, ORG, 'gone')).rejects.toMatchObject({ status: 404 });
51
+ });
52
+ });
53
+ describe('git history & content', () => {
54
+ it('listCommits passes ref/limit as query and unwraps {commits}', async () => {
55
+ fetchMock.mockResolvedValueOnce(ok({ commits: [{ sha: 'abc', message: 'init' }] }));
56
+ const commits = await git.listCommits(API_KEY, ORG, 'app', { ref: 'dev', limit: 10 });
57
+ expect(commits).toHaveLength(1);
58
+ const url = fetchMock.mock.calls[0][0];
59
+ expect(url).toMatch(/\/commits\?/);
60
+ expect(url).toContain('ref=dev');
61
+ expect(url).toContain('limit=10');
62
+ });
63
+ it('getDiff requires base+head and unwraps {diff}', async () => {
64
+ fetchMock.mockResolvedValueOnce(ok({ diff: '--- a\n+++ b\n' }));
65
+ const d = await git.getDiff(API_KEY, ORG, 'app', 'main', 'dev');
66
+ expect(d).toBe('--- a\n+++ b\n');
67
+ const url = fetchMock.mock.calls[0][0];
68
+ expect(url).toContain('base=main');
69
+ expect(url).toContain('head=dev');
70
+ });
71
+ it('listTree unwraps {entries} and adds ?path when scoped', async () => {
72
+ fetchMock.mockResolvedValueOnce(ok({ entries: [{ name: 'a.js', type: 'file' }] }));
73
+ await git.listTree(API_KEY, ORG, 'app', 'main', 'src');
74
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/tree\/main\?path=src$/);
75
+ });
76
+ it('readBlob encodes path segments but keeps the slashes', async () => {
77
+ fetchMock.mockResolvedValueOnce(ok({ path: 'src/a b.js', size: 3, content_base64: 'eA==' }));
78
+ await git.readBlob(API_KEY, ORG, 'app', 'main', 'src/a b.js');
79
+ // slashes survive (catch-all route), spaces within a segment are encoded
80
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/blob\/main\/src\/a%20b\.js$/);
81
+ });
82
+ });
83
+ describe('git writes', () => {
84
+ it('commit POSTs the full payload', async () => {
85
+ fetchMock.mockResolvedValueOnce(ok({ sha: 'def', tree: 't', branch: 'main' }, 201));
86
+ const res = await git.commit(API_KEY, ORG, 'app', {
87
+ branch: 'main', message: 'add', changes: [{ path: 'x', content: 'hi' }],
88
+ });
89
+ expect(res.sha).toBe('def');
90
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body).changes).toHaveLength(1);
91
+ });
92
+ it('createBranch POSTs {name, from}', async () => {
93
+ fetchMock.mockResolvedValueOnce(ok({ name: 'feature' }, 201));
94
+ await git.createBranch(API_KEY, ORG, 'app', 'feature', 'main');
95
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ name: 'feature', from: 'main' });
96
+ });
97
+ it('merge POSTs {target, source} and surfaces NOT_FAST_FORWARD (422)', async () => {
98
+ fetchMock.mockResolvedValueOnce(ok({ sha: 'ff' }));
99
+ expect((await git.merge(API_KEY, ORG, 'app', 'main', 'dev')).sha).toBe('ff');
100
+ fetchMock.mockResolvedValueOnce(fail('NOT_FAST_FORWARD', 422));
101
+ await expect(git.merge(API_KEY, ORG, 'app', 'main', 'dev')).rejects.toMatchObject({ status: 422 });
102
+ });
103
+ it('repack POSTs and returns the pack counts', async () => {
104
+ fetchMock.mockResolvedValueOnce(ok({ packs_before: 5, packs_after: 1, objects: 200 }));
105
+ const r = await git.repack(API_KEY, ORG, 'app');
106
+ expect(r).toEqual({ packs_before: 5, packs_after: 1, objects: 200 });
107
+ });
108
+ });
109
+ describe('git.EXPOSES', () => {
110
+ it('covers all 16 git endpoints', () => {
111
+ expect(git.EXPOSES).toHaveLength(16);
112
+ expect(git.EXPOSES).toContain('POST /git/orgs/{org_id}/repos/{repo}/commits');
113
+ expect(git.EXPOSES).toContain('GET /git/orgs/{org_id}/repos/{repo}/blob/{ref}/{path}');
114
+ });
115
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ // SDK-level unit tests for the queue module. Verifies URL/body shape,
2
+ // envelope-unwrapping, query params, and error handling against
3
+ // myapi-hq/internal/routes/queue/. Mocks global fetch — no network.
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { queue } from '@myapihq/sdk';
6
+ const API_KEY = 'myapi_test_abc';
7
+ const ORG = '11111111-1111-4111-8111-111111111111';
8
+ let fetchMock;
9
+ function ok(data, status = 200) {
10
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
11
+ status, headers: { 'content-type': 'application/json' },
12
+ });
13
+ }
14
+ function fail(code, status) {
15
+ return new Response(JSON.stringify({ success: false, error: { code, message: code }, meta: {} }), {
16
+ status, headers: { 'content-type': 'application/json' },
17
+ });
18
+ }
19
+ beforeEach(() => { fetchMock = vi.fn(); globalThis.fetch = fetchMock; });
20
+ afterEach(() => { vi.restoreAllMocks(); });
21
+ describe('queue queues', () => {
22
+ it('createQueue POSTs {name, consumer_url} and optional policy fields', async () => {
23
+ fetchMock.mockResolvedValueOnce(ok({ name: 'q', consumer_url: 'https://c', max_attempts: 5, max_concurrency: 5 }, 201));
24
+ await queue.createQueue(API_KEY, ORG, {
25
+ name: 'q', consumerUrl: 'https://c', maxAttempts: 3, maxConcurrency: 2,
26
+ });
27
+ const [url, init] = fetchMock.mock.calls[0];
28
+ expect(url).toContain(`/queue/orgs/${ORG}/queues`);
29
+ expect(init.method).toBe('POST');
30
+ expect(JSON.parse(init.body)).toEqual({
31
+ name: 'q', consumer_url: 'https://c', max_attempts: 3, max_concurrency: 2,
32
+ });
33
+ });
34
+ it('createQueue omits unset optional fields', async () => {
35
+ fetchMock.mockResolvedValueOnce(ok({ name: 'q', consumer_url: 'https://c', max_attempts: 5, max_concurrency: 5 }, 201));
36
+ await queue.createQueue(API_KEY, ORG, { name: 'q', consumerUrl: 'https://c' });
37
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ name: 'q', consumer_url: 'https://c' });
38
+ });
39
+ it('listQueues unwraps the {queues:[...]} envelope to an array', async () => {
40
+ fetchMock.mockResolvedValueOnce(ok({ queues: [{ name: 'a' }, { name: 'b' }] }));
41
+ expect(await queue.listQueues(API_KEY, ORG)).toEqual([{ name: 'a' }, { name: 'b' }]);
42
+ });
43
+ it('getQueue GETs /queues/{name}', async () => {
44
+ fetchMock.mockResolvedValueOnce(ok({ name: 'q', consumer_url: 'https://c', max_attempts: 5, max_concurrency: 5 }));
45
+ const q = await queue.getQueue(API_KEY, ORG, 'q');
46
+ expect(q.max_attempts).toBe(5);
47
+ expect(fetchMock.mock.calls[0][0]).toContain(`/queue/orgs/${ORG}/queues/q`);
48
+ });
49
+ });
50
+ describe('queue jobs', () => {
51
+ it('enqueueJob POSTs payload/dedup_key/delay_seconds/depends_on', async () => {
52
+ fetchMock.mockResolvedValueOnce(ok({ id: 'j1', queue: 'q', status: 'pending', attempts: 0 }, 201));
53
+ await queue.enqueueJob(API_KEY, ORG, 'q', {
54
+ payload: { x: 1 }, dedupKey: 'k', delaySeconds: 30, dependsOn: ['j0'],
55
+ });
56
+ const [url, init] = fetchMock.mock.calls[0];
57
+ expect(url).toContain(`/queue/orgs/${ORG}/queues/q/jobs`);
58
+ expect(init.method).toBe('POST');
59
+ expect(JSON.parse(init.body)).toEqual({
60
+ payload: { x: 1 }, dedup_key: 'k', delay_seconds: 30, depends_on: ['j0'],
61
+ });
62
+ });
63
+ it('listJobs unwraps {jobs} and passes status/limit as query', async () => {
64
+ fetchMock.mockResolvedValueOnce(ok({ jobs: [{ id: 'j1', status: 'dead' }] }));
65
+ const jobs = await queue.listJobs(API_KEY, ORG, 'q', { status: 'dead', limit: 50 });
66
+ expect(jobs).toHaveLength(1);
67
+ const url = fetchMock.mock.calls[0][0];
68
+ expect(url).toMatch(/\/jobs\?/);
69
+ expect(url).toContain('status=dead');
70
+ expect(url).toContain('limit=50');
71
+ });
72
+ it('getJob GETs /jobs/{id} and surfaces 404', async () => {
73
+ fetchMock.mockResolvedValueOnce(ok({ id: 'j1', queue: 'q', status: 'succeeded', attempts: 1 }));
74
+ expect((await queue.getJob(API_KEY, ORG, 'j1')).status).toBe('succeeded');
75
+ expect(fetchMock.mock.calls[0][0]).toContain(`/queue/orgs/${ORG}/jobs/j1`);
76
+ fetchMock.mockResolvedValueOnce(fail('job not found', 404));
77
+ await expect(queue.getJob(API_KEY, ORG, 'gone')).rejects.toMatchObject({ status: 404 });
78
+ });
79
+ });
80
+ describe('queue.EXPOSES', () => {
81
+ it('covers all 6 queue endpoints', () => {
82
+ expect(queue.EXPOSES).toHaveLength(6);
83
+ expect(queue.EXPOSES).toContain('POST /queue/orgs/{org_id}/queues/{name}/jobs');
84
+ expect(queue.EXPOSES).toContain('GET /queue/orgs/{org_id}/jobs/{id}');
85
+ });
86
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ // SDK-level unit tests for the task module. Verifies URL/body shape,
2
+ // envelope-unwrapping, the list meta {shown,total_open}, the separate body
3
+ // tier, and lease modeling against myapi-hq/internal/routes/task/.
4
+ // Mocks global fetch — no network.
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { task } from '@myapihq/sdk';
7
+ const API_KEY = 'myapi_test_abc';
8
+ const ORG = '11111111-1111-4111-8111-111111111111';
9
+ let fetchMock;
10
+ function ok(data, status = 200) {
11
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
12
+ status, headers: { 'content-type': 'application/json' },
13
+ });
14
+ }
15
+ function fail(code, status) {
16
+ return new Response(JSON.stringify({ success: false, error: { code, message: code }, meta: {} }), {
17
+ status, headers: { 'content-type': 'application/json' },
18
+ });
19
+ }
20
+ beforeEach(() => { fetchMock = vi.fn(); globalThis.fetch = fetchMock; });
21
+ afterEach(() => { vi.restoreAllMocks(); });
22
+ describe('task create / list', () => {
23
+ it('createTask POSTs {description} plus only the set optional fields', async () => {
24
+ fetchMock.mockResolvedValueOnce(ok({ id: 't1', description: 'do x', status: 'open' }, 201));
25
+ await task.createTask(API_KEY, ORG, {
26
+ description: 'do x', importance: 'high', dependsOn: ['t0'],
27
+ resolveOn: { event_type: 'payment.succeeded', field: 'id', value: '42' },
28
+ });
29
+ const [url, init] = fetchMock.mock.calls[0];
30
+ expect(url).toContain(`/task/orgs/${ORG}/tasks`);
31
+ expect(init.method).toBe('POST');
32
+ expect(JSON.parse(init.body)).toEqual({
33
+ description: 'do x', importance: 'high', depends_on: ['t0'],
34
+ resolve_on: { event_type: 'payment.succeeded', field: 'id', value: '42' },
35
+ });
36
+ });
37
+ it('listTasks reads tasks + data.meta {shown, total_open}', async () => {
38
+ fetchMock.mockResolvedValueOnce(ok({ tasks: [{ id: 't1', description: 'a', score: 9 }], meta: { shown: 1, total_open: 12 } }));
39
+ const res = await task.listTasks(API_KEY, ORG);
40
+ expect(res.tasks).toHaveLength(1);
41
+ expect(res.meta).toEqual({ shown: 1, total_open: 12 });
42
+ });
43
+ it('listTasks passes filter options as query params', async () => {
44
+ fetchMock.mockResolvedValueOnce(ok({ tasks: [], meta: { shown: 0, total_open: 0 } }));
45
+ await task.listTasks(API_KEY, ORG, { status: 'open', limit: 5 });
46
+ const url = fetchMock.mock.calls[0][0];
47
+ expect(url).toContain('status=open');
48
+ expect(url).toContain('limit=5');
49
+ });
50
+ });
51
+ describe('task get / body tier', () => {
52
+ it('getTask GETs /tasks/{id} and does NOT touch the body endpoint', async () => {
53
+ fetchMock.mockResolvedValueOnce(ok({ id: 't1', description: 'a', status: 'open' }));
54
+ await task.getTask(API_KEY, ORG, 't1');
55
+ const url = fetchMock.mock.calls[0][0];
56
+ expect(url).toContain(`/task/orgs/${ORG}/tasks/t1`);
57
+ expect(url).not.toMatch(/\/body$/);
58
+ });
59
+ it('getTaskBody fetches the /body tier and unwraps {task_id, body}', async () => {
60
+ fetchMock.mockResolvedValueOnce(ok({ task_id: 't1', body: '# context' }));
61
+ expect(await task.getTaskBody(API_KEY, ORG, 't1')).toBe('# context');
62
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/tasks\/t1\/body$/);
63
+ });
64
+ });
65
+ describe('task lifecycle', () => {
66
+ it('claimTask POSTs {lease_seconds, worker} and returns the full task', async () => {
67
+ fetchMock.mockResolvedValueOnce(ok({
68
+ id: 't1', description: 'a', status: 'claimed',
69
+ lease_expires_at: '2026-05-19T10:10:00Z', claimed_by: 'agent-7',
70
+ }));
71
+ const t = await task.claimTask(API_KEY, ORG, 't1', { leaseSeconds: 600, worker: 'agent-7' });
72
+ expect(t.status).toBe('claimed');
73
+ expect(t.lease_expires_at).toBe('2026-05-19T10:10:00Z');
74
+ expect(t.claimed_by).toBe('agent-7');
75
+ const [url, init] = fetchMock.mock.calls[0];
76
+ expect(url).toMatch(/\/tasks\/t1\/claim$/);
77
+ expect(JSON.parse(init.body)).toEqual({ lease_seconds: 600, worker: 'agent-7' });
78
+ });
79
+ it('extendTask POSTs to /extend', async () => {
80
+ fetchMock.mockResolvedValueOnce(ok({ id: 't1', description: 'a', status: 'claimed' }));
81
+ await task.extendTask(API_KEY, ORG, 't1', { leaseSeconds: 300 });
82
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/tasks\/t1\/extend$/);
83
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ lease_seconds: 300 });
84
+ });
85
+ it('resolveTask POSTs to /resolve', async () => {
86
+ fetchMock.mockResolvedValueOnce(ok({ id: 't1', description: 'a', status: 'resolved' }));
87
+ await task.resolveTask(API_KEY, ORG, 't1');
88
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/tasks\/t1\/resolve$/);
89
+ });
90
+ it('failTask POSTs {reason} to /fail', async () => {
91
+ fetchMock.mockResolvedValueOnce(ok({ id: 't1', description: 'a', status: 'failed' }));
92
+ await task.failTask(API_KEY, ORG, 't1', 'blocked on legal');
93
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/tasks\/t1\/fail$/);
94
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ reason: 'blocked on legal' });
95
+ });
96
+ it('cancelTask DELETEs the task and surfaces 404', async () => {
97
+ fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
98
+ await task.cancelTask(API_KEY, ORG, 't1');
99
+ expect(fetchMock.mock.calls[0][1].method).toBe('DELETE');
100
+ fetchMock.mockResolvedValueOnce(fail('task not found', 404));
101
+ await expect(task.cancelTask(API_KEY, ORG, 'gone')).rejects.toMatchObject({ status: 404 });
102
+ });
103
+ });
104
+ describe('task.EXPOSES', () => {
105
+ it('covers all 9 task endpoints', () => {
106
+ expect(task.EXPOSES).toHaveLength(9);
107
+ expect(task.EXPOSES).toContain('GET /task/orgs/{org_id}/tasks/{id}/body');
108
+ expect(task.EXPOSES).toContain('POST /task/orgs/{org_id}/tasks/{id}/claim');
109
+ });
110
+ });
@@ -0,0 +1,45 @@
1
+ ---
2
+ # my-email-api
3
+
4
+ Send transactional email and run drip campaigns from mailboxes on your own registered domains. Includes AI template generation, warmup, and inbox/outbox reading.
5
+
6
+ ## What it does
7
+
8
+ - Create mailboxes on your registered domains
9
+ - Send transactional emails (one-shot or templated)
10
+ - Read inbox, outbox, sent history, and per-message status
11
+ - Generate HTML email templates with AI from a prompt
12
+ - Run paced drip campaigns against uploaded contact lists
13
+ - Manage IP/domain warmup for sender reputation
14
+
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ # Create a mailbox + activate sending
19
+ myapi email mailbox create hello@yourdomain.com
20
+ myapi email mailbox activate-sending --address hello@yourdomain.com
21
+
22
+ # Send
23
+ myapi email message send \
24
+ --from hello@yourdomain.com \
25
+ --to recipient@example.com \
26
+ --subject "Hi" \
27
+ --body "Test"
28
+ ```
29
+
30
+ ## Authentication
31
+
32
+ ```bash
33
+ export MYAPI_KEY=mak_...
34
+ ```
35
+
36
+ Requires:
37
+ - An `api_key` from **myapihq**
38
+ - A registered domain via **mydomainapi**, assigned to your org
39
+ - Default `org_id` (for templates/campaigns) — set with `myapi auth config set-org <id>`
40
+
41
+ ## Documentation
42
+
43
+ Full command reference and flow diagrams: see `SKILL.md`.
44
+
45
+ Run `myapi email --help` for inline reference.