@myapihq/cli 1.1.0-wip.2 → 1.1.0-wip.4
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/funnel.js +24 -5
- package/dist/commands/image.d.ts +4 -4
- package/dist/commands/image.js +143 -51
- package/dist/commands/storage.d.ts +4 -4
- package/dist/commands/storage.js +117 -23
- package/dist/commands/workflow.js +92 -15
- package/dist/flags.js +23 -1
- package/dist/index.js +14 -0
- package/dist/output.d.ts +1 -0
- package/dist/output.js +8 -1
- package/dist/skills/my-image-api/README.md +32 -0
- package/dist/skills/my-image-api/SKILL.md +74 -0
- package/dist/skills/my-image-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-image-api/make/.gitkeep +0 -0
- package/dist/skills/my-image-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-image-api/openapi/.gitkeep +0 -0
- package/dist/skills/my-storage-api/README.md +35 -0
- package/dist/skills/my-storage-api/SKILL.md +88 -0
- package/dist/skills/my-storage-api/claude/.claude-plugin/plugin.json +6 -0
- package/dist/skills/my-storage-api/make/.gitkeep +0 -0
- package/dist/skills/my-storage-api/n8n/.gitkeep +0 -0
- package/dist/skills/my-storage-api/openapi/.gitkeep +0 -0
- package/dist/utils.js +3 -3
- package/package.json +1 -1
package/dist/commands/funnel.js
CHANGED
|
@@ -108,13 +108,27 @@ export async function push(slug, flags) {
|
|
|
108
108
|
if (resolvedFromOrg) {
|
|
109
109
|
info(`(Used the org's only funnel: ${funnelId}. Set a default with: myapi config set-funnel ${funnelId})`);
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
111
|
+
let liveUrl = result?.url;
|
|
112
|
+
if (!liveUrl) {
|
|
115
113
|
const org = await hq.getOrg(config.api_key, orgId);
|
|
116
114
|
if (org.preview_subdomain) {
|
|
117
|
-
|
|
115
|
+
liveUrl = `https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (liveUrl)
|
|
119
|
+
info(`Preview: ${liveUrl}`);
|
|
120
|
+
// If we're still on the preview subdomain, hint at how to serve on a
|
|
121
|
+
// custom domain. Tailor the hint: if a default_domain is already
|
|
122
|
+
// configured (registered + meant to be used), suggest assigning it
|
|
123
|
+
// directly; otherwise point at register + assign.
|
|
124
|
+
const onPreview = liveUrl?.includes('.makeautonomous.com');
|
|
125
|
+
if (onPreview) {
|
|
126
|
+
const dom = config.default_domain;
|
|
127
|
+
if (dom) {
|
|
128
|
+
info(`(Serving on the preview subdomain. To serve on ${dom} instead: myapi domain assign ${dom})`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
info(`(Serving on the preview subdomain. To serve on a custom domain: myapi domain register <domain> && myapi domain assign <domain>)`);
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -149,6 +163,11 @@ const SUBCOMMAND_USAGE = {
|
|
|
149
163
|
Reads HTML from stdin and publishes to <slug> on your funnel. Funnel is resolved
|
|
150
164
|
from --funnel, the default funnel, or (only if the org has exactly one) auto-picked.
|
|
151
165
|
|
|
166
|
+
By default, your funnel is served on a preview subdomain (*.makeautonomous.com).
|
|
167
|
+
To serve on your own domain, register and assign one with:
|
|
168
|
+
myapi domain register <domain>
|
|
169
|
+
myapi domain assign <domain>
|
|
170
|
+
|
|
152
171
|
Examples:
|
|
153
172
|
echo '<h1>Hello</h1>' | myapi funnel push /
|
|
154
173
|
cat about.html | myapi funnel push /about
|
package/dist/commands/image.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export declare
|
|
4
|
-
export declare function run(subcommand: string | undefined, args: string[], flags:
|
|
1
|
+
import type { FlagSchema } from '../flags.js';
|
|
2
|
+
import { type Flags } from '../helpers.js';
|
|
3
|
+
export declare const SCHEMA: FlagSchema;
|
|
4
|
+
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/image.js
CHANGED
|
@@ -1,70 +1,157 @@
|
|
|
1
1
|
import { image as sdkImage } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
|
-
import { success, error, printTable, info, printJson } from '../output.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
4
|
+
import { formatDate, pollJob } from '../utils.js';
|
|
5
|
+
import { requireOrg, requireArg } from '../helpers.js';
|
|
6
|
+
export const SCHEMA = {
|
|
7
|
+
org: 'string',
|
|
8
|
+
prompt: 'string',
|
|
9
|
+
ratio: 'string',
|
|
10
|
+
style: 'string',
|
|
11
|
+
colors: 'string',
|
|
12
|
+
text: 'boolean',
|
|
13
|
+
};
|
|
14
|
+
const VALID_RATIOS = new Set(['1:1', '16:9', '9:16', '4:3', '3:4']);
|
|
15
|
+
// Hex list: optional leading #, 3 or 6 hex digits, comma-separated.
|
|
16
|
+
const HEX_LIST_RE = /^#?[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?(?:\s*,\s*#?[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?)*$/;
|
|
17
|
+
const PRICE_USD = 0.05;
|
|
18
|
+
function summarizeImage(j) {
|
|
19
|
+
const prompt = (j.prompt || '');
|
|
20
|
+
const truncated = prompt.length > 60 ? prompt.slice(0, 60) + '...' : prompt;
|
|
21
|
+
return {
|
|
22
|
+
job_id: j.job_id,
|
|
23
|
+
status: j.status,
|
|
24
|
+
aspect_ratio: j.aspect_ratio,
|
|
25
|
+
prompt: truncated,
|
|
26
|
+
url: j.url || '',
|
|
27
|
+
created_at: j.created_at ? formatDate(j.created_at) : '',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function generate(promptArg, flags) {
|
|
6
31
|
const config = requireConfig();
|
|
7
|
-
const orgId = flags
|
|
8
|
-
|
|
9
|
-
|
|
32
|
+
const orgId = requireOrg(flags, config, 'myapi image generate <prompt> [--ratio 1:1|16:9|9:16|4:3|3:4] [--style <s>] [--colors <hex,hex>] [--text] [--org <id>]');
|
|
33
|
+
// Convention: first required arg is positional. --prompt still works for back-compat.
|
|
34
|
+
const prompt = promptArg || flags.prompt;
|
|
35
|
+
if (!prompt) {
|
|
36
|
+
error('Missing required arguments.\nUsage: myapi image generate <prompt> [--ratio <ratio>] [--style <s>] [--colors <c>] [--text] [--org <id>]\n or: myapi image generate --prompt "<text>" ...');
|
|
37
|
+
}
|
|
38
|
+
// Client-side validation: catch typos before they round-trip.
|
|
39
|
+
if (flags.ratio && !VALID_RATIOS.has(flags.ratio)) {
|
|
40
|
+
error(`Invalid --ratio "${flags.ratio}". Allowed: ${[...VALID_RATIOS].join(', ')}`);
|
|
10
41
|
}
|
|
11
|
-
|
|
42
|
+
if (flags.colors && !HEX_LIST_RE.test(flags.colors)) {
|
|
43
|
+
error(`Invalid --colors "${flags.colors}". Expected comma-separated hex like "#ff6600,#003366" (3- or 6-digit, # optional).`);
|
|
44
|
+
}
|
|
45
|
+
const payload = { prompt };
|
|
12
46
|
if (flags.ratio)
|
|
13
47
|
payload.aspect_ratio = flags.ratio;
|
|
14
48
|
if (flags.style)
|
|
15
49
|
payload.style = flags.style;
|
|
16
50
|
if (flags.colors)
|
|
17
51
|
payload.colors = flags.colors;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
process.stdout.write(`\rGenerating image ${chars[i++ % chars.length]}`);
|
|
38
|
-
await sleep(3000);
|
|
39
|
-
elapsed += 3000;
|
|
52
|
+
if (flags.text)
|
|
53
|
+
payload.has_text = true;
|
|
54
|
+
// Cost preview before kickoff. Goes to stderr (banner) so --json output
|
|
55
|
+
// stays clean for piping.
|
|
56
|
+
banner(`› Charging $${PRICE_USD.toFixed(2)} for this image generation…`);
|
|
57
|
+
const job = await sdkImage.generateImage(config.api_key, orgId, payload);
|
|
58
|
+
const final = await pollJob({
|
|
59
|
+
label: 'Generating image',
|
|
60
|
+
timeoutMs: 90_000,
|
|
61
|
+
intervalMs: 3000,
|
|
62
|
+
check: () => sdkImage.getImageJob(config.api_key, orgId, job.job_id),
|
|
63
|
+
isDone: s => s.status === 'completed',
|
|
64
|
+
isFailed: s => s.status === 'failed',
|
|
65
|
+
failedMessage: 'Image generation failed',
|
|
66
|
+
timeoutMessage: 'Image generation timed out — re-run "myapi image get <id>" to check later.',
|
|
67
|
+
});
|
|
68
|
+
if (flags.json) {
|
|
69
|
+
printJson(final);
|
|
70
|
+
return;
|
|
40
71
|
}
|
|
41
|
-
|
|
42
|
-
error("Image generation timed out");
|
|
72
|
+
success(`Image generated!\nID: ${final.job_id}\nURL: ${final.url}`);
|
|
43
73
|
}
|
|
44
|
-
|
|
74
|
+
async function list(flags) {
|
|
45
75
|
const config = requireConfig();
|
|
46
|
-
const orgId = flags
|
|
47
|
-
if (!orgId)
|
|
48
|
-
error("Missing required arguments.\nUsage: myapi image list --org <id>\n(Or set defaults via: myapi config set-org <id>)");
|
|
76
|
+
const orgId = requireOrg(flags, config, 'myapi image list [--org <id>]');
|
|
49
77
|
const images = await sdkImage.listImages(config.api_key, orgId);
|
|
50
|
-
if (flags.json)
|
|
78
|
+
if (flags.json) {
|
|
51
79
|
printJson(images);
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
printTable(images.map(summarizeImage), {
|
|
83
|
+
flags,
|
|
84
|
+
empty: 'No images yet. Generate one: myapi image generate "<prompt>"',
|
|
85
|
+
});
|
|
54
86
|
}
|
|
55
|
-
|
|
87
|
+
async function get(id, flags) {
|
|
56
88
|
const config = requireConfig();
|
|
57
|
-
const orgId = flags
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
const orgId = requireOrg(flags, config, 'myapi image get <job_id> [--org <id>] [--json]');
|
|
90
|
+
requireArg(id, 'job_id', 'myapi image get <job_id> [--org <id>] [--json]');
|
|
91
|
+
const job = await sdkImage.getImageJob(config.api_key, orgId, id);
|
|
92
|
+
if (flags.json) {
|
|
93
|
+
printJson(job);
|
|
94
|
+
return;
|
|
60
95
|
}
|
|
96
|
+
info(`ID: ${job.job_id}`);
|
|
97
|
+
info(`Status: ${job.status}`);
|
|
98
|
+
info(`Aspect ratio: ${job.aspect_ratio}`);
|
|
99
|
+
if (job.prompt)
|
|
100
|
+
info(`Prompt: ${job.prompt}`);
|
|
101
|
+
if (job.url)
|
|
102
|
+
info(`URL: ${job.url}`);
|
|
103
|
+
if (job.error)
|
|
104
|
+
info(`Error: ${job.error}`);
|
|
105
|
+
if (job.created_at)
|
|
106
|
+
info(`Created: ${formatDate(job.created_at)}`);
|
|
107
|
+
}
|
|
108
|
+
// `get-url`: just the URL string, nothing else. Curl-friendly. Errors if
|
|
109
|
+
// the job isn't completed (no URL to print).
|
|
110
|
+
async function getUrl(id, _flags) {
|
|
111
|
+
const config = requireConfig();
|
|
112
|
+
const orgId = requireOrg(_flags, config, 'myapi image get-url <job_id> [--org <id>]');
|
|
113
|
+
requireArg(id, 'job_id', 'myapi image get-url <job_id> [--org <id>]');
|
|
114
|
+
const job = await sdkImage.getImageJob(config.api_key, orgId, id);
|
|
115
|
+
if (!job.url) {
|
|
116
|
+
error(`Image ${id} has no URL yet (status: ${job.status}). Re-check with: myapi image get ${id}`);
|
|
117
|
+
}
|
|
118
|
+
console.log(job.url);
|
|
119
|
+
}
|
|
120
|
+
async function del(id, flags) {
|
|
121
|
+
const config = requireConfig();
|
|
122
|
+
const orgId = requireOrg(flags, config, 'myapi image delete <job_id> [--org <id>]');
|
|
123
|
+
requireArg(id, 'job_id', 'myapi image delete <job_id> [--org <id>]');
|
|
61
124
|
await sdkImage.deleteImage(config.api_key, orgId, id);
|
|
62
|
-
success(`Image
|
|
125
|
+
success(`Image ${id} deleted (job history retained, URL nullified)`);
|
|
63
126
|
}
|
|
64
127
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
65
128
|
const SUBCOMMAND_USAGE = {
|
|
66
129
|
'list': 'myapi image list [--org <id>] [--json]',
|
|
67
|
-
'generate':
|
|
130
|
+
'generate': `myapi image generate <prompt> [--ratio 1:1|16:9|9:16|4:3|3:4] [--style <s>] [--colors <hex,hex>] [--text] [--org <id>]
|
|
131
|
+
myapi image generate --prompt "<text>" ...
|
|
132
|
+
|
|
133
|
+
Either form works; the positional prompt is the recommended shape.
|
|
134
|
+
|
|
135
|
+
Generation is asynchronous — the CLI polls for up to 90s. If it times out
|
|
136
|
+
the job keeps running server-side; re-fetch with:
|
|
137
|
+
myapi image get <job_id>
|
|
138
|
+
|
|
139
|
+
Cost: $${PRICE_USD.toFixed(2)} per generation. Failed jobs aren't charged.
|
|
140
|
+
|
|
141
|
+
Flags:
|
|
142
|
+
--ratio Aspect ratio (allowed: ${[...VALID_RATIOS].join(', ')}; default 1:1)
|
|
143
|
+
--style Free-form style hint (e.g. "watercolor", "cyberpunk neon")
|
|
144
|
+
--colors Hex colors comma-separated (e.g. "#ff6600,#003366")
|
|
145
|
+
--text Allow text in the image (off by default — text rarely renders well)`,
|
|
146
|
+
'get': `myapi image get <job_id> [--org <id>] [--json]
|
|
147
|
+
|
|
148
|
+
Round-trips the API to fetch the full job: status, URL, prompt, aspect ratio,
|
|
149
|
+
created_at. JSON output via --json; otherwise human-readable block.`,
|
|
150
|
+
'get-url': `myapi image get-url <job_id> [--org <id>]
|
|
151
|
+
|
|
152
|
+
Prints just the asset URL. Errors if the job hasn't completed yet.
|
|
153
|
+
Curl-friendly:
|
|
154
|
+
curl -O "$(myapi image get-url <id>)"`,
|
|
68
155
|
'delete': 'myapi image delete <job_id> [--org <id>]',
|
|
69
156
|
};
|
|
70
157
|
export async function run(subcommand, args, flags) {
|
|
@@ -72,11 +159,14 @@ export async function run(subcommand, args, flags) {
|
|
|
72
159
|
info(`Usage: myapi image <subcommand>
|
|
73
160
|
|
|
74
161
|
Subcommands:
|
|
75
|
-
list
|
|
76
|
-
generate Generate a new AI image
|
|
77
|
-
|
|
162
|
+
list List all your generated images
|
|
163
|
+
generate <prompt> Generate a new AI image (async, polls up to 90s, $${PRICE_USD.toFixed(2)})
|
|
164
|
+
get <job_id> Fetch full job metadata (JSON or human-readable)
|
|
165
|
+
get-url <job_id> Print just the asset URL (curl-friendly)
|
|
166
|
+
delete <job_id> Delete the asset (job history retained)
|
|
78
167
|
|
|
79
|
-
All commands accept --org <id> (or set default: myapi config set-org <id>)
|
|
168
|
+
All commands accept --org <id> (or set default: myapi config set-org <id>).
|
|
169
|
+
The asset lands in your org's storage and is also visible via "myapi storage list".`);
|
|
80
170
|
return;
|
|
81
171
|
}
|
|
82
172
|
if (flags.help) {
|
|
@@ -84,13 +174,15 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
84
174
|
if (usage)
|
|
85
175
|
info(`Usage: ${usage}`);
|
|
86
176
|
else
|
|
87
|
-
|
|
177
|
+
error(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for the list.`);
|
|
88
178
|
return;
|
|
89
179
|
}
|
|
90
180
|
switch (subcommand) {
|
|
91
181
|
case 'list': return list(flags);
|
|
92
|
-
case 'generate': return generate(flags);
|
|
182
|
+
case 'generate': return generate(args[0], flags);
|
|
183
|
+
case 'get': return get(args[0], flags);
|
|
184
|
+
case 'get-url': return getUrl(args[0], flags);
|
|
93
185
|
case 'delete': return del(args[0], flags);
|
|
94
|
-
default: error(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for
|
|
186
|
+
default: error(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for available subcommands.`);
|
|
95
187
|
}
|
|
96
188
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export declare
|
|
4
|
-
export declare function run(subcommand: string | undefined, args: string[], flags:
|
|
1
|
+
import type { FlagSchema } from '../flags.js';
|
|
2
|
+
import { type Flags } from '../helpers.js';
|
|
3
|
+
export declare const SCHEMA: FlagSchema;
|
|
4
|
+
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/storage.js
CHANGED
|
@@ -1,39 +1,127 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { extname, basename } from 'path';
|
|
3
|
+
import { storage as sdkStorage, STORAGE_BASE } from '@myapihq/sdk';
|
|
2
4
|
import { requireConfig } from '../config.js';
|
|
3
5
|
import { success, error, printTable, info, printJson } from '../output.js';
|
|
4
|
-
|
|
6
|
+
import { formatDate } from '../utils.js';
|
|
7
|
+
import { requireOrg, requireArg } from '../helpers.js';
|
|
8
|
+
export const SCHEMA = {
|
|
9
|
+
org: 'string',
|
|
10
|
+
name: 'string',
|
|
11
|
+
};
|
|
12
|
+
// Mirrors UploadContentType in @myapihq/sdk + backend allowlist.
|
|
13
|
+
const EXT_TO_CT = {
|
|
14
|
+
'.png': 'image/png',
|
|
15
|
+
'.jpg': 'image/jpeg',
|
|
16
|
+
'.jpeg': 'image/jpeg',
|
|
17
|
+
'.gif': 'image/gif',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
};
|
|
20
|
+
function summarizeAsset(a) {
|
|
21
|
+
return {
|
|
22
|
+
asset_id: a.asset_id,
|
|
23
|
+
name: a.name || '(unnamed)',
|
|
24
|
+
url: a.url,
|
|
25
|
+
created_at: a.created_at ? formatDate(a.created_at) : '',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function list(flags) {
|
|
5
29
|
const config = requireConfig();
|
|
6
|
-
const orgId = flags
|
|
7
|
-
if (!orgId)
|
|
8
|
-
error("Missing required arguments.\nUsage: myapi storage list --org <id>\n(Or set defaults via: myapi config set-org <id>)");
|
|
30
|
+
const orgId = requireOrg(flags, config, 'myapi storage list [--org <id>]');
|
|
9
31
|
const assets = await sdkStorage.listAssets(config.api_key, orgId);
|
|
10
|
-
if (flags.json)
|
|
32
|
+
if (flags.json) {
|
|
11
33
|
printJson(assets);
|
|
12
|
-
|
|
13
|
-
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
printTable(assets.map(summarizeAsset), {
|
|
37
|
+
flags,
|
|
38
|
+
empty: 'No assets yet. Upload one: myapi storage upload <file> · Or ingest from URL: myapi storage ingest <url>',
|
|
39
|
+
});
|
|
14
40
|
}
|
|
15
|
-
|
|
41
|
+
async function ingest(url, flags) {
|
|
16
42
|
const config = requireConfig();
|
|
17
|
-
const orgId = flags
|
|
18
|
-
|
|
19
|
-
error("Missing required arguments.\nUsage: myapi storage ingest <url> [--name <name>] --org <id>\n(Or set defaults via: myapi config set-org <id>)");
|
|
20
|
-
}
|
|
43
|
+
const orgId = requireOrg(flags, config, 'myapi storage ingest <url> [--name <name>] [--org <id>]');
|
|
44
|
+
requireArg(url, 'url', 'myapi storage ingest <url> [--name <name>] [--org <id>]');
|
|
21
45
|
const res = await sdkStorage.ingestAsset(config.api_key, orgId, url, flags.name);
|
|
22
46
|
success(`Asset ingested! ID: ${res.asset_id}\nHosted URL: ${res.url}`);
|
|
23
47
|
}
|
|
24
|
-
|
|
48
|
+
async function upload(filePath, flags) {
|
|
25
49
|
const config = requireConfig();
|
|
26
|
-
const orgId = flags
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
const orgId = requireOrg(flags, config, 'myapi storage upload <file> [--name <name>] [--org <id>]');
|
|
51
|
+
requireArg(filePath, 'file', 'myapi storage upload <file> [--name <name>] [--org <id>]');
|
|
52
|
+
const ext = extname(filePath).toLowerCase();
|
|
53
|
+
const contentType = EXT_TO_CT[ext];
|
|
54
|
+
if (!contentType) {
|
|
55
|
+
error(`Unsupported file extension "${ext}". Supported: ${Object.keys(EXT_TO_CT).join(', ')}.\n(SVG and non-image types — pdf/mp4/webm — go through "myapi storage ingest <url>" today.)`);
|
|
56
|
+
}
|
|
57
|
+
let data;
|
|
58
|
+
try {
|
|
59
|
+
data = await readFile(filePath);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
if (e?.code === 'ENOENT')
|
|
63
|
+
error(`File not found: ${filePath}`);
|
|
64
|
+
if (e?.code === 'EACCES')
|
|
65
|
+
error(`Permission denied: ${filePath}`);
|
|
66
|
+
error(`Could not read ${filePath}: ${e?.message ?? e}`);
|
|
29
67
|
}
|
|
68
|
+
const name = flags.name || basename(filePath);
|
|
69
|
+
const res = await sdkStorage.uploadAsset(config.api_key, orgId, data, contentType, name);
|
|
70
|
+
success(`Asset uploaded! ID: ${res.asset_id}\nHosted URL: ${res.url}`);
|
|
71
|
+
}
|
|
72
|
+
// `get`: round-trip metadata for one asset. Backend has no single-asset
|
|
73
|
+
// GET endpoint today, so we list + filter locally. Switch to a direct
|
|
74
|
+
// call once the backend exposes one.
|
|
75
|
+
async function get(id, flags) {
|
|
76
|
+
const config = requireConfig();
|
|
77
|
+
const orgId = requireOrg(flags, config, 'myapi storage get <asset_id> [--org <id>]');
|
|
78
|
+
requireArg(id, 'asset_id', 'myapi storage get <asset_id> [--org <id>]');
|
|
79
|
+
const all = await sdkStorage.listAssets(config.api_key, orgId);
|
|
80
|
+
const asset = all.find(a => a.asset_id === id);
|
|
81
|
+
if (!asset)
|
|
82
|
+
error(`Asset ${id} not found in this org. List with: myapi storage list`);
|
|
83
|
+
if (flags.json) {
|
|
84
|
+
printJson(asset);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
info(`ID: ${asset.asset_id}`);
|
|
88
|
+
info(`Name: ${asset.name || '(unnamed)'}`);
|
|
89
|
+
info(`URL: ${asset.url}`);
|
|
90
|
+
if (asset.created_at)
|
|
91
|
+
info(`Created: ${formatDate(asset.created_at)}`);
|
|
92
|
+
}
|
|
93
|
+
// `get-url`: cheapest possible — print the public URL pattern. No API
|
|
94
|
+
// call. Curl-friendly. Doesn't even verify the asset exists; that's a
|
|
95
|
+
// trade-off for being a pure local URL constructor.
|
|
96
|
+
async function getUrl(id, _flags) {
|
|
97
|
+
requireArg(id, 'asset_id', 'myapi storage get-url <asset_id>');
|
|
98
|
+
console.log(`${STORAGE_BASE}/storage/${encodeURIComponent(id)}`);
|
|
99
|
+
}
|
|
100
|
+
async function del(id, flags) {
|
|
101
|
+
const config = requireConfig();
|
|
102
|
+
const orgId = requireOrg(flags, config, 'myapi storage delete <asset_id> [--org <id>]');
|
|
103
|
+
requireArg(id, 'asset_id', 'myapi storage delete <asset_id> [--org <id>]');
|
|
30
104
|
await sdkStorage.deleteAsset(config.api_key, orgId, id);
|
|
31
105
|
success(`Asset ${id} deleted`);
|
|
32
106
|
}
|
|
33
107
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
34
108
|
const SUBCOMMAND_USAGE = {
|
|
35
109
|
'list': 'myapi storage list [--org <id>] [--json]',
|
|
36
|
-
'ingest':
|
|
110
|
+
'ingest': `myapi storage ingest <url> [--name <name>] [--org <id>]
|
|
111
|
+
|
|
112
|
+
Server fetches the URL and stores the file. Useful for migrating assets that
|
|
113
|
+
already live on a public URL (e.g. importing brand assets from another host).`,
|
|
114
|
+
'upload': `myapi storage upload <file> [--name <name>] [--org <id>]
|
|
115
|
+
|
|
116
|
+
Direct multipart upload of a local file. Supported: ${Object.keys(EXT_TO_CT).join(', ')}.
|
|
117
|
+
The display name defaults to the file's basename — override with --name.`,
|
|
118
|
+
'get': `myapi storage get <asset_id> [--org <id>] [--json]
|
|
119
|
+
|
|
120
|
+
Round-trips the API to fetch the asset's metadata (name, URL, created_at).`,
|
|
121
|
+
'get-url': `myapi storage get-url <asset_id>
|
|
122
|
+
|
|
123
|
+
Prints the public CDN URL with no API call. Curl-friendly:
|
|
124
|
+
curl -O "$(myapi storage get-url <id>)"`,
|
|
37
125
|
'delete': 'myapi storage delete <asset_id> [--org <id>]',
|
|
38
126
|
};
|
|
39
127
|
export async function run(subcommand, args, flags) {
|
|
@@ -41,9 +129,12 @@ export async function run(subcommand, args, flags) {
|
|
|
41
129
|
info(`Usage: myapi storage <subcommand>
|
|
42
130
|
|
|
43
131
|
Subcommands:
|
|
44
|
-
list
|
|
45
|
-
ingest
|
|
46
|
-
|
|
132
|
+
list List all your stored assets
|
|
133
|
+
ingest <url> Server pulls a public URL into storage
|
|
134
|
+
upload <file> Direct upload of a local file
|
|
135
|
+
get <asset_id> Fetch asset metadata (name, URL, created_at)
|
|
136
|
+
get-url <asset_id> Print the public CDN URL (no API call)
|
|
137
|
+
delete <asset_id> Permanently delete a stored asset
|
|
47
138
|
|
|
48
139
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
49
140
|
return;
|
|
@@ -53,13 +144,16 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
53
144
|
if (usage)
|
|
54
145
|
info(`Usage: ${usage}`);
|
|
55
146
|
else
|
|
56
|
-
|
|
147
|
+
error(`Unknown subcommand: ${subcommand}. Run "myapi storage --help" for the list.`);
|
|
57
148
|
return;
|
|
58
149
|
}
|
|
59
150
|
switch (subcommand) {
|
|
60
151
|
case 'list': return list(flags);
|
|
61
152
|
case 'ingest': return ingest(args[0], flags);
|
|
153
|
+
case 'upload': return upload(args[0], flags);
|
|
154
|
+
case 'get': return get(args[0], flags);
|
|
155
|
+
case 'get-url': return getUrl(args[0], flags);
|
|
62
156
|
case 'delete': return del(args[0], flags);
|
|
63
|
-
default: error(`Unknown subcommand: ${subcommand}. Run "myapi storage --help" for
|
|
157
|
+
default: error(`Unknown subcommand: ${subcommand}. Run "myapi storage --help" for available subcommands.`);
|
|
64
158
|
}
|
|
65
159
|
}
|
|
@@ -13,16 +13,56 @@ export const SCHEMA = {
|
|
|
13
13
|
// forms are accepted by the workflow runner. Keep this in sync if the
|
|
14
14
|
// backend grows new step types.
|
|
15
15
|
const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack'];
|
|
16
|
+
const SLACK_HOOK_RE = /^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+/;
|
|
17
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
// Per-step-type required fields. Validated client-side so typos and
|
|
19
|
+
// hallucinated shapes fail fast, before the network call. Backend
|
|
20
|
+
// performs the same validation as defense in depth.
|
|
16
21
|
function validateSteps(steps) {
|
|
17
22
|
if (!Array.isArray(steps))
|
|
18
23
|
error('--steps must be a JSON array of step objects.');
|
|
24
|
+
if (steps.length === 0)
|
|
25
|
+
error('--steps cannot be an empty array.');
|
|
19
26
|
steps.forEach((s, i) => {
|
|
27
|
+
const where = `step ${i}`;
|
|
20
28
|
if (!s || typeof s !== 'object')
|
|
21
|
-
error(
|
|
29
|
+
error(`${where}: must be a JSON object.`);
|
|
22
30
|
if (!s.type)
|
|
23
|
-
error(
|
|
31
|
+
error(`${where}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
|
|
24
32
|
if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
|
|
25
|
-
error(
|
|
33
|
+
error(`${where}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
if (s.type === 'send_email' || s.type === 'email') {
|
|
36
|
+
const required = ['from', 'to', 'subject'];
|
|
37
|
+
for (const f of required) {
|
|
38
|
+
if (!s[f] || typeof s[f] !== 'string') {
|
|
39
|
+
error(`${where} (${s.type}): missing required field "${f}".`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const bodyForms = ['body', 'html', 'template_id'].filter(f => s[f] !== undefined && s[f] !== '');
|
|
43
|
+
if (bodyForms.length === 0) {
|
|
44
|
+
error(`${where} (${s.type}): must include exactly one of "body", "html", or "template_id".`);
|
|
45
|
+
}
|
|
46
|
+
if (bodyForms.length > 1) {
|
|
47
|
+
error(`${where} (${s.type}): can only use one of "body", "html", "template_id" — got: ${bodyForms.join(', ')}.`);
|
|
48
|
+
}
|
|
49
|
+
if (s.template_vars !== undefined && !s.template_id) {
|
|
50
|
+
error(`${where} (${s.type}): "template_vars" only makes sense with "template_id".`);
|
|
51
|
+
}
|
|
52
|
+
if (s.template_vars !== undefined && (typeof s.template_vars !== 'object' || Array.isArray(s.template_vars))) {
|
|
53
|
+
error(`${where} (${s.type}): "template_vars" must be a JSON object.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (s.type === 'slack_message' || s.type === 'slack') {
|
|
57
|
+
if (!s.webhook_url || typeof s.webhook_url !== 'string') {
|
|
58
|
+
error(`${where} (${s.type}): missing required field "webhook_url".`);
|
|
59
|
+
}
|
|
60
|
+
if (!SLACK_HOOK_RE.test(s.webhook_url)) {
|
|
61
|
+
error(`${where} (${s.type}): "webhook_url" must look like https://hooks.slack.com/services/T.../B.../xxx — got "${s.webhook_url}".`);
|
|
62
|
+
}
|
|
63
|
+
if (!s.text || typeof s.text !== 'string') {
|
|
64
|
+
error(`${where} (${s.type}): missing required field "text".`);
|
|
65
|
+
}
|
|
26
66
|
}
|
|
27
67
|
});
|
|
28
68
|
}
|
|
@@ -77,6 +117,9 @@ export async function create(nameArg, flags) {
|
|
|
77
117
|
if (!name || !endpointId || !flags.steps) {
|
|
78
118
|
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>]');
|
|
79
119
|
}
|
|
120
|
+
if (!UUID_RE.test(endpointId)) {
|
|
121
|
+
error(`Invalid --endpoint-id "${endpointId}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
|
|
122
|
+
}
|
|
80
123
|
let steps;
|
|
81
124
|
try {
|
|
82
125
|
steps = JSON.parse(flags.steps);
|
|
@@ -107,8 +150,13 @@ export async function update(id, flags) {
|
|
|
107
150
|
const payload = {};
|
|
108
151
|
if (typeof flags.name === 'string')
|
|
109
152
|
payload.name = flags.name;
|
|
110
|
-
if (typeof flags['endpoint-id'] === 'string')
|
|
111
|
-
|
|
153
|
+
if (typeof flags['endpoint-id'] === 'string') {
|
|
154
|
+
const eid = flags['endpoint-id'];
|
|
155
|
+
if (!UUID_RE.test(eid)) {
|
|
156
|
+
error(`Invalid --endpoint-id "${eid}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
|
|
157
|
+
}
|
|
158
|
+
payload.trigger_config = { endpoint_id: eid };
|
|
159
|
+
}
|
|
112
160
|
if (typeof flags.steps === 'string') {
|
|
113
161
|
try {
|
|
114
162
|
payload.steps = JSON.parse(flags.steps);
|
|
@@ -180,19 +228,48 @@ const SUBCOMMAND_USAGE = {
|
|
|
180
228
|
|
|
181
229
|
Either form works; the positional name is the recommended shape.
|
|
182
230
|
|
|
183
|
-
--steps is a JSON array of step objects.
|
|
184
|
-
|
|
185
|
-
|
|
231
|
+
--steps is a JSON array of step objects. Each step has a "type" field
|
|
232
|
+
plus type-specific fields. Two types are supported today:
|
|
233
|
+
|
|
234
|
+
type: "send_email" (alias: "email")
|
|
235
|
+
Required:
|
|
236
|
+
from Sender mailbox address (must be activated for sending)
|
|
237
|
+
to Recipient address (templating allowed)
|
|
238
|
+
subject Email subject (templating allowed)
|
|
239
|
+
Body — exactly one of:
|
|
240
|
+
body Plain text body
|
|
241
|
+
html Raw HTML body
|
|
242
|
+
template_id UUID of an AI-generated template (myapi email template
|
|
243
|
+
generate). Recommended for nice-looking emails.
|
|
244
|
+
Optional:
|
|
245
|
+
template_vars JSON object of variable substitutions, only with
|
|
246
|
+
template_id (e.g. {"name": "{{ payload.name }}"}).
|
|
247
|
+
|
|
248
|
+
type: "slack_message" (alias: "slack")
|
|
249
|
+
Required:
|
|
250
|
+
webhook_url https://hooks.slack.com/services/T.../B.../xxx
|
|
251
|
+
Get one from https://api.slack.com/apps → your app →
|
|
252
|
+
Incoming Webhooks. Other URLs are rejected.
|
|
253
|
+
text Message text (templating allowed)
|
|
254
|
+
|
|
255
|
+
Templating: any string field can reference the incoming webhook payload
|
|
256
|
+
with {{ payload.field }} (whitespace optional). Example:
|
|
257
|
+
"to": "{{ payload.email }}" → the email field from the form submission.
|
|
258
|
+
|
|
259
|
+
Unknown step types AND missing/invalid fields are rejected at create
|
|
260
|
+
time — you find out about typos before any workflow run happens.
|
|
186
261
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
reference the incoming webhook payload, e.g. "to": "{{ payload.email }}".
|
|
190
|
-
Unknown step types are rejected at create time, not at execute time.
|
|
262
|
+
There is NO on-failure notification mechanism today. Inspect run history
|
|
263
|
+
with: myapi workflow runs <id> · myapi workflow get-run <run_id>.
|
|
191
264
|
|
|
192
|
-
|
|
265
|
+
Examples:
|
|
266
|
+
# send a thank-you email using an AI template, then ping Slack
|
|
193
267
|
myapi workflow create "Contact handler" --endpoint-id <wid> --steps '[
|
|
194
|
-
{"type":"
|
|
195
|
-
"subject":"Thanks!","
|
|
268
|
+
{"type":"email","from":"hello@x.com","to":"{{ payload.email }}",
|
|
269
|
+
"subject":"Thanks!","template_id":"<tid>",
|
|
270
|
+
"template_vars":{"name":"{{ payload.name }}"}},
|
|
271
|
+
{"type":"slack","webhook_url":"https://hooks.slack.com/services/T0/B0/xxx",
|
|
272
|
+
"text":"New: {{ payload.message }}"}
|
|
196
273
|
]'
|
|
197
274
|
|
|
198
275
|
See the my-workflow-api skill for the full form-to-email recipe.`,
|
package/dist/flags.js
CHANGED
|
@@ -16,6 +16,7 @@ export function parseFlags(argv, schema = {}) {
|
|
|
16
16
|
const merged = { ...GLOBAL_FLAGS, ...schema };
|
|
17
17
|
const args = [];
|
|
18
18
|
const flags = {};
|
|
19
|
+
const unknownFlags = [];
|
|
19
20
|
for (let i = 0; i < argv.length; i++) {
|
|
20
21
|
const arg = argv[i];
|
|
21
22
|
if (arg === '-h') {
|
|
@@ -44,9 +45,24 @@ export function parseFlags(argv, schema = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
const type = merged[key];
|
|
46
47
|
// Unknown flags: tolerate as boolean (with --key=value still honored)
|
|
47
|
-
// so additions to the schema don't silently break someone's script
|
|
48
|
+
// so additions to the schema don't silently break someone's script —
|
|
49
|
+
// BUT also collect them and emit a stderr warning at the end of
|
|
50
|
+
// parsing. Surfaces typos and AI-agent hallucinations like
|
|
51
|
+
// `--on-error <email>` without breaking back-compat.
|
|
48
52
|
if (type === undefined) {
|
|
49
53
|
flags[key] = inlineValue ?? true;
|
|
54
|
+
unknownFlags.push(`--${key}`);
|
|
55
|
+
// If no inline value and the next token doesn't look like another
|
|
56
|
+
// flag, the user almost certainly intended it as the flag's value
|
|
57
|
+
// (e.g. `--on-error someone@x.com`). Consume it so it doesn't
|
|
58
|
+
// become a stray positional arg later.
|
|
59
|
+
if (inlineValue === undefined) {
|
|
60
|
+
const next = argv[i + 1];
|
|
61
|
+
if (next !== undefined && !next.startsWith('--') && !next.startsWith('-h') && !next.startsWith('-v')) {
|
|
62
|
+
flags[key] = next;
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
50
66
|
continue;
|
|
51
67
|
}
|
|
52
68
|
if (type === 'boolean') {
|
|
@@ -84,5 +100,11 @@ export function parseFlags(argv, schema = {}) {
|
|
|
84
100
|
}
|
|
85
101
|
args.push(arg);
|
|
86
102
|
}
|
|
103
|
+
// Surface unknown flags so typos and hallucinated flags don't silently
|
|
104
|
+
// disappear. Don't block — the value is still in `flags` for any handler
|
|
105
|
+
// that wants it — just print one line on stderr.
|
|
106
|
+
if (unknownFlags.length > 0 && !process.env.MYAPI_QUIET_UNKNOWN_FLAGS) {
|
|
107
|
+
process.stderr.write(`› Note: ignoring unknown flag(s): ${unknownFlags.join(', ')}\n`);
|
|
108
|
+
}
|
|
87
109
|
return { args, flags };
|
|
88
110
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import * as funnelCmd from './commands/funnel.js';
|
|
|
16
16
|
import * as webhookCmd from './commands/webhook.js';
|
|
17
17
|
import * as workflowCmd from './commands/workflow.js';
|
|
18
18
|
import * as emailCmd from './commands/email/index.js';
|
|
19
|
+
import * as imageCmd from './commands/image.js';
|
|
20
|
+
import * as storageCmd from './commands/storage.js';
|
|
19
21
|
import * as authCmd from './commands/auth.js';
|
|
20
22
|
import * as configCmd from './commands/config.js';
|
|
21
23
|
// Each command file declares the value flags it understands. We union them
|
|
@@ -28,8 +30,10 @@ const COMBINED_SCHEMA = {
|
|
|
28
30
|
...domainCmd.SCHEMA,
|
|
29
31
|
...emailCmd.SCHEMA,
|
|
30
32
|
...funnelCmd.SCHEMA,
|
|
33
|
+
...imageCmd.SCHEMA,
|
|
31
34
|
...keysCmd.SCHEMA,
|
|
32
35
|
...orgCmd.SCHEMA,
|
|
36
|
+
...storageCmd.SCHEMA,
|
|
33
37
|
...webhookCmd.SCHEMA,
|
|
34
38
|
...workflowCmd.SCHEMA,
|
|
35
39
|
// Top-level flags
|
|
@@ -120,6 +124,12 @@ async function main() {
|
|
|
120
124
|
case 'email':
|
|
121
125
|
await emailCmd.run(subcommand, restArgs, flags);
|
|
122
126
|
break;
|
|
127
|
+
case 'image':
|
|
128
|
+
await imageCmd.run(subcommand, restArgs, flags);
|
|
129
|
+
break;
|
|
130
|
+
case 'storage':
|
|
131
|
+
await storageCmd.run(subcommand, restArgs, flags);
|
|
132
|
+
break;
|
|
123
133
|
// Convenience aliases
|
|
124
134
|
case 'setup':
|
|
125
135
|
await setupCmd.setup(flags);
|
|
@@ -202,6 +212,8 @@ const HELP_TARGETS = {
|
|
|
202
212
|
webhook: f => webhookCmd.run(undefined, [], f),
|
|
203
213
|
workflow: f => workflowCmd.run(undefined, [], f),
|
|
204
214
|
email: f => emailCmd.run(undefined, [], f),
|
|
215
|
+
image: f => imageCmd.run(undefined, [], f),
|
|
216
|
+
storage: f => storageCmd.run(undefined, [], f),
|
|
205
217
|
org: f => orgCmd.run(undefined, [], f),
|
|
206
218
|
billing: f => billingCmd.run(undefined, [], f),
|
|
207
219
|
keys: f => keysCmd.run(undefined, [], f),
|
|
@@ -244,6 +256,8 @@ Commands:
|
|
|
244
256
|
webhook Manage inbound webhook endpoints and inspect deliveries
|
|
245
257
|
email Manage mailboxes, send/read email, templates, and campaigns
|
|
246
258
|
workflow Run actions (send email, post to Slack) when a webhook fires
|
|
259
|
+
image Generate AI images and manage them in storage
|
|
260
|
+
storage Upload, ingest, list, and serve assets from edge storage
|
|
247
261
|
|
|
248
262
|
Aliases:
|
|
249
263
|
whoami → myapi auth whoami
|
package/dist/output.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare function info(message: string): void;
|
|
|
7
7
|
export declare function banner(message: string): void;
|
|
8
8
|
export declare function printJson(data: unknown): void;
|
|
9
9
|
export declare function spinnerFrame(i: number): string;
|
|
10
|
+
export declare function spinnerWrite(s: string): void;
|
|
10
11
|
export declare function clearLine(): void;
|
|
11
12
|
export interface PrintTableOptions {
|
|
12
13
|
/** Pass `flags` so `--json` (any truthy form) routes to JSON output. */
|
package/dist/output.js
CHANGED
|
@@ -17,12 +17,19 @@ export function printJson(data) {
|
|
|
17
17
|
}
|
|
18
18
|
// Spinner / line-clear primitives. Used by polling helpers (utils.pollJob)
|
|
19
19
|
// and any handler that wants its own progress UI.
|
|
20
|
+
//
|
|
21
|
+
// Spinner writes go to STDERR — keeping stdout clean for whatever the
|
|
22
|
+
// command's actual output is (JSON, an id, a URL). Otherwise spinner bytes
|
|
23
|
+
// pollute `--json` parsing or piped output.
|
|
20
24
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
21
25
|
export function spinnerFrame(i) {
|
|
22
26
|
return SPINNER_FRAMES[i % SPINNER_FRAMES.length];
|
|
23
27
|
}
|
|
28
|
+
export function spinnerWrite(s) {
|
|
29
|
+
process.stderr.write(s);
|
|
30
|
+
}
|
|
24
31
|
export function clearLine() {
|
|
25
|
-
process.
|
|
32
|
+
process.stderr.write('\r\x1b[K');
|
|
26
33
|
}
|
|
27
34
|
// Generic so callers don't need `as unknown as Record<string, unknown>[]`.
|
|
28
35
|
// Field names come from the first row; if SDK renames a field, the projector
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
# my-image-api
|
|
3
|
+
|
|
4
|
+
Generate AI images from a text prompt. Async — submit, poll, get a public CDN URL. Asset auto-saved to your org's storage.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
- Text-to-image generation with prompt, aspect ratio, style, colors, optional text
|
|
9
|
+
- Async pipeline (~10–30s typical, 90s timeout)
|
|
10
|
+
- Output stored in mystorageapi automatically
|
|
11
|
+
- Public CDN URL for embedding in funnels, emails, anywhere
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
myapi image generate "A clean flat-color logo for a sustainable jam company"
|
|
17
|
+
# → polls, then prints the asset URL
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export MYAPI_KEY=mak_...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires `api_key` and `org_id` from **myapihq**. ~$0.05 per generation.
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
Full command reference, flags, and async semantics: see `SKILL.md`.
|
|
31
|
+
|
|
32
|
+
Run `myapi image --help` for inline reference.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-image-api
|
|
3
|
+
description: >
|
|
4
|
+
Generate AI images from a text prompt. Async — submit a prompt, the CLI polls until the image is ready, then returns a public CDN URL. Image lands automatically in your org's storage.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MyImageAPI
|
|
8
|
+
|
|
9
|
+
Text-to-image generation. Submit a prompt with optional aspect ratio, style, color palette, and "allow text" hint. The job is asynchronous — the CLI polls until completion (typically 10–30s, capped at 90s).
|
|
10
|
+
|
|
11
|
+
## How It Fits Together
|
|
12
|
+
|
|
13
|
+
- Requires `api_key` and `org_id` from **myapihq**.
|
|
14
|
+
- Each generation costs ~$0.05 (deducted from your balance).
|
|
15
|
+
- The output asset is stored in your org's **mystorageapi** bucket — `myapi storage list` and `myapi storage get` can fetch it later by id.
|
|
16
|
+
- Generated images can be referenced from a **myfunnelapi** page or embedded in a **myemailapi** template.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Generate (positional prompt is the recommended shape)
|
|
22
|
+
myapi image generate "A clean flat-color logo for a sustainable jam company"
|
|
23
|
+
|
|
24
|
+
# Same with options
|
|
25
|
+
myapi image generate "Hero image, 16:9, mountains at dawn" \
|
|
26
|
+
--ratio 16:9 --style "watercolor" --colors "#ff6600,#003366"
|
|
27
|
+
|
|
28
|
+
# List your generations
|
|
29
|
+
myapi image list
|
|
30
|
+
|
|
31
|
+
# Inspect or fetch a specific job
|
|
32
|
+
myapi image get <job_id>
|
|
33
|
+
|
|
34
|
+
# Delete the asset (the job history record stays)
|
|
35
|
+
myapi image delete <job_id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## All Commands
|
|
39
|
+
|
|
40
|
+
| Command | What it does |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `myapi image generate <prompt>` | Async generate, polls up to 90s, returns id + URL |
|
|
43
|
+
| `myapi image list` | List all generated images for the org |
|
|
44
|
+
| `myapi image get <job_id>` | Get full job details (status, URL, prompt, aspect ratio) |
|
|
45
|
+
| `myapi image delete <job_id>` | Delete the asset (job record kept for history) |
|
|
46
|
+
|
|
47
|
+
## Generate Flags
|
|
48
|
+
|
|
49
|
+
| Flag | Allowed | Default | Notes |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `--ratio` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4` | `1:1` | Aspect ratio of the output |
|
|
52
|
+
| `--style` | free-form string | none | Style hint, e.g. `"watercolor"`, `"cyberpunk neon"` |
|
|
53
|
+
| `--colors` | hex list, comma-separated | none | e.g. `"#ff6600,#003366"` — the model will bias toward these |
|
|
54
|
+
| `--text` | flag (no value) | off | Allow text in the image. Off by default — text rarely renders well, use only when you specifically want a logo or sign |
|
|
55
|
+
|
|
56
|
+
## Async Behavior
|
|
57
|
+
|
|
58
|
+
Generation is async. The CLI:
|
|
59
|
+
1. Submits the prompt → gets a `job_id` immediately.
|
|
60
|
+
2. Polls `GET /image/orgs/<org>/jobs/<job_id>` every 3 seconds.
|
|
61
|
+
3. Returns when status is `completed` (success) or `failed` (errors out).
|
|
62
|
+
4. If 90 seconds elapse without completion, the CLI prints a timeout message but **the job keeps running server-side**. Check back with:
|
|
63
|
+
```
|
|
64
|
+
myapi image get <job_id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
- Generations cost $0.05 each. Failed jobs aren't charged.
|
|
70
|
+
- The asset URL is public (anyone with the URL can view) — don't generate sensitive content.
|
|
71
|
+
- Prompts with explicit text usually fail; for branded text use a separate text overlay step or a real designer.
|
|
72
|
+
- Delete is permanent for the asset; the job's prompt + metadata stays for your history (`image list` will still show it with an empty URL).
|
|
73
|
+
|
|
74
|
+
Run `myapi image --help` or `myapi image <subcommand> --help` for full flag reference.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
# my-storage-api
|
|
3
|
+
|
|
4
|
+
Edge-hosted asset storage. Upload local files or ingest from URLs. Each asset gets a stable public CDN URL.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
- Direct multipart upload of `.png` / `.jpg` files
|
|
9
|
+
- Ingest from any public URL (server pulls)
|
|
10
|
+
- Public CDN delivery — stable URL until deletion
|
|
11
|
+
- Used automatically by my-image-api for generated images
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
myapi storage upload ./logo.png
|
|
17
|
+
# → asset uploaded! URL: https://api.mystorageapi.com/storage/<id>
|
|
18
|
+
|
|
19
|
+
# Or fetch via curl
|
|
20
|
+
curl -O "$(myapi storage get <asset_id>)"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export MYAPI_KEY=mak_...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires `api_key` and `org_id` from **myapihq**.
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
Full command reference, upload-vs-ingest tradeoffs, and naming conventions: see `SKILL.md`.
|
|
34
|
+
|
|
35
|
+
Run `myapi storage --help` for inline reference.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-storage-api
|
|
3
|
+
description: >
|
|
4
|
+
Edge-hosted asset storage. Upload local files (.png/.jpg) directly, or have the server fetch from a public URL. Each asset gets a stable public CDN URL.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MyStorageAPI
|
|
8
|
+
|
|
9
|
+
Per-org asset storage with edge CDN delivery. Two ways in:
|
|
10
|
+
- **Direct upload** — push a local file (multipart upload)
|
|
11
|
+
- **Ingest from URL** — server fetches a public URL and stores the file
|
|
12
|
+
|
|
13
|
+
Both produce a stable public URL like `https://api.mystorageapi.com/storage/<id>`.
|
|
14
|
+
|
|
15
|
+
## How It Fits Together
|
|
16
|
+
|
|
17
|
+
- Requires `api_key` and `org_id` from **myapihq**.
|
|
18
|
+
- Upload supports `.png`, `.jpg`, `.jpeg` today. (Add more via `mystorageapi`'s backend if you need them.)
|
|
19
|
+
- Assets are public — anyone with the URL can fetch.
|
|
20
|
+
- **myimageapi** automatically uses storage for generated images, so generated images appear in `myapi storage list` too.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Direct upload
|
|
26
|
+
myapi storage upload ./logo.png --name "brand-logo"
|
|
27
|
+
|
|
28
|
+
# Ingest a public URL (server pulls)
|
|
29
|
+
myapi storage ingest https://example.com/hero.jpg --name "hero"
|
|
30
|
+
|
|
31
|
+
# List
|
|
32
|
+
myapi storage list
|
|
33
|
+
|
|
34
|
+
# Get the public URL (curl-friendly)
|
|
35
|
+
curl -O "$(myapi storage get <asset_id>)"
|
|
36
|
+
|
|
37
|
+
# Delete
|
|
38
|
+
myapi storage delete <asset_id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## All Commands
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `myapi storage list` | List all stored assets |
|
|
46
|
+
| `myapi storage upload <file>` | Direct multipart upload of a local file |
|
|
47
|
+
| `myapi storage ingest <url>` | Server fetches a public URL into storage |
|
|
48
|
+
| `myapi storage get <asset_id>` | Print the public CDN URL (no API call) |
|
|
49
|
+
| `myapi storage delete <asset_id>` | Permanently delete the asset |
|
|
50
|
+
|
|
51
|
+
## Upload vs Ingest
|
|
52
|
+
|
|
53
|
+
Pick based on where the file is:
|
|
54
|
+
|
|
55
|
+
- **`upload`** — file is on your machine. Multipart POST. Constraint: `.png` / `.jpg` / `.jpeg` only today.
|
|
56
|
+
- **`ingest`** — file is at a public HTTP(S) URL. Server downloads and stores. Useful for migrating assets from another host or pulling in third-party images you have rights to.
|
|
57
|
+
|
|
58
|
+
Both produce identical asset records — `list` doesn't distinguish them.
|
|
59
|
+
|
|
60
|
+
## Naming
|
|
61
|
+
|
|
62
|
+
Both `upload` and `ingest` accept `--name <display>` for a human-friendly label. If omitted:
|
|
63
|
+
- `upload` defaults to the filename's basename.
|
|
64
|
+
- `ingest` defaults to the URL path's filename or empty.
|
|
65
|
+
|
|
66
|
+
The display name is for your reference only — the public URL uses the auto-generated id.
|
|
67
|
+
|
|
68
|
+
## Public URLs
|
|
69
|
+
|
|
70
|
+
`myapi storage get <id>` is a pure-local URL constructor — no API call, no auth, no rate limit. The output is exactly:
|
|
71
|
+
```
|
|
72
|
+
https://api.mystorageapi.com/storage/<id>
|
|
73
|
+
```
|
|
74
|
+
(or whatever `MYAPI_STORAGE_URL` is set to). Pipe it directly into curl:
|
|
75
|
+
```bash
|
|
76
|
+
curl -O "$(myapi storage get abc123)"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The URL itself is permanent until you `myapi storage delete <id>` — embed it freely in your funnels, emails, or anywhere else.
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- Assets are public by default. Don't store sensitive files.
|
|
84
|
+
- Delete is immediate and unrecoverable.
|
|
85
|
+
- Generated images from **myimageapi** show up in `storage list` under their job id.
|
|
86
|
+
- If you need a non-image format (PDF, video, etc.), `ingest` works as long as the server-side storage accepts it; `upload` is restricted to image types.
|
|
87
|
+
|
|
88
|
+
Run `myapi storage --help` for full flag reference.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spinnerFrame, clearLine, error } from './output.js';
|
|
1
|
+
import { spinnerFrame, spinnerWrite, clearLine, error } from './output.js';
|
|
2
2
|
export function sleep(ms) {
|
|
3
3
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
4
4
|
}
|
|
@@ -23,7 +23,7 @@ export function formatDate(str) {
|
|
|
23
23
|
export async function pollJob(opts) {
|
|
24
24
|
const timeoutMs = opts.timeoutMs ?? 90_000;
|
|
25
25
|
const intervalMs = opts.intervalMs ?? 3_000;
|
|
26
|
-
|
|
26
|
+
spinnerWrite(`${opts.label} `);
|
|
27
27
|
let i = 0;
|
|
28
28
|
let elapsed = 0;
|
|
29
29
|
while (elapsed < timeoutMs) {
|
|
@@ -36,7 +36,7 @@ export async function pollJob(opts) {
|
|
|
36
36
|
clearLine();
|
|
37
37
|
error(opts.failedMessage ?? `${opts.label} failed`);
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
spinnerWrite(`\r${opts.label} ${spinnerFrame(i++)}`);
|
|
40
40
|
await sleep(intervalMs);
|
|
41
41
|
elapsed += intervalMs;
|
|
42
42
|
}
|