@myapihq/cli 1.2.9 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/container.d.ts +1 -0
- package/dist/commands/container.js +41 -0
- package/dist/commands/email/mailbox.js +28 -1
- package/dist/commands/queue-validation.test.d.ts +1 -0
- package/dist/commands/queue-validation.test.js +38 -0
- package/dist/commands/queue.d.ts +14 -0
- package/dist/commands/queue.js +215 -0
- package/dist/commands/task-validation.test.d.ts +1 -0
- package/dist/commands/task-validation.test.js +37 -0
- package/dist/commands/task.d.ts +18 -0
- package/dist/commands/task.js +288 -0
- package/dist/commands/workflow-validation.test.js +27 -0
- package/dist/commands/workflow.js +17 -1
- package/dist/completion.js +5 -3
- package/dist/exposes.test.js +2 -0
- package/dist/index.js +14 -0
- package/dist/sdk-container.test.js +35 -1
- package/dist/sdk-email-forwarding.test.d.ts +1 -0
- package/dist/sdk-email-forwarding.test.js +48 -0
- package/dist/sdk-queue.test.d.ts +1 -0
- package/dist/sdk-queue.test.js +86 -0
- package/dist/sdk-task.test.d.ts +1 -0
- package/dist/sdk-task.test.js +110 -0
- package/dist/skills/my-domain-api/SKILL.md +2 -0
- package/dist/skills/my-workflow-api/SKILL.md +10 -1
- package/package.json +2 -2
|
@@ -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
|
+
});
|
|
@@ -15,6 +15,8 @@ Handles domain registration, assignment to orgs, and edge (CDN/security) setting
|
|
|
15
15
|
<!-- llm:start -->
|
|
16
16
|
Domains are how you take a funnel from `your-org.makeautonomous.com` to `your-real-brand.com`. The flow is: check availability, register (deducts credits), assign to an org, watch status until DNS propagates. From that point, your org's funnel serves at `https://yourdomain.com`. SSL provisions automatically a few minutes after status flips to `active`.
|
|
17
17
|
|
|
18
|
+
A registered domain isn't only for funnels: a **deployed container** can be served on a custom domain or subdomain too — bind it with `myapi container domain <id> <domain>` (see `my-container-api`). Funnels are static sites; containers are dynamic apps. Either way, the parent domain must be registered here first.
|
|
19
|
+
|
|
18
20
|
You can also import existing domains (without re-registering) and tune CDN/security settings per-domain.
|
|
19
21
|
|
|
20
22
|
Without a domain, funnels still work on the free `*.makeautonomous.com` preview subdomain.
|
|
@@ -21,8 +21,10 @@ Step types today:
|
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| `send_email` | `email` | `from`, `to`, `subject`, plus one of `body` / `html` / `template_id` |
|
|
23
23
|
| `slack_message` | `slack` | `webhook_url`, `text` |
|
|
24
|
+
| `http_request` | `http` | `url` (optional: `method`, `body`, `headers`) |
|
|
25
|
+
| `enqueue_job` | `enqueue` | `queue` (optional: `payload`, `dedup_key`, `delay_seconds`) — hands durable work to **my-queue-api** |
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
All 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.
|
|
26
28
|
|
|
27
29
|
Unknown step types are rejected at create time, so typos surface immediately rather than after 3 failed retries during execution.
|
|
28
30
|
|
|
@@ -95,4 +97,11 @@ myapi workflow enable <id>
|
|
|
95
97
|
- Disable to stop firing without losing the configuration.
|
|
96
98
|
- Deleting a workflow purges its run history; the webhook endpoint remains.
|
|
97
99
|
|
|
100
|
+
## See also
|
|
101
|
+
|
|
102
|
+
`workflow` is one of three orchestration slots. Use **workflow** to react to
|
|
103
|
+
inbound webhooks; use **my-queue-api** for durable retried machine work; use
|
|
104
|
+
**my-task-api** for work that needs an agent/human decision. Full comparison:
|
|
105
|
+
`docs/orchestration-decision-guide.md`.
|
|
106
|
+
|
|
98
107
|
Run `myapi workflow --help` for full flag reference.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"lint:changelog": "node ../../scripts/lint-changelog.js"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@myapihq/sdk": "^1.
|
|
32
|
+
"@myapihq/sdk": "^1.3.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^25.6.0",
|