@myapihq/cli 1.1.0-wip.3 → 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/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/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/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
|
}
|
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
|
}
|