@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.
@@ -1,4 +1,4 @@
1
- export declare function generate(flags: Record<string, string | boolean>): Promise<void>;
2
- export declare function list(flags: Record<string, string | boolean>): Promise<void>;
3
- export declare function del(id: string, flags: Record<string, string | boolean>): Promise<void>;
4
- export declare function run(subcommand: string | undefined, args: string[], flags: Record<string, string | boolean>): Promise<void>;
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>;
@@ -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 { sleep } from '../utils.js';
5
- export async function generate(flags) {
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.org || config.default_org;
8
- if (!orgId || !flags.prompt) {
9
- error("Missing required arguments.\nUsage: myapi image generate --prompt <text> --org <id>\n(Or set defaults via: myapi config set-org <id>)");
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
- const payload = { prompt: flags.prompt };
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
- const res = await sdkImage.generateImage(config.api_key, orgId, payload);
19
- process.stdout.write("Generating image ");
20
- const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
21
- let i = 0;
22
- let elapsed = 0;
23
- while (elapsed < 60000) {
24
- const job = await sdkImage.getImageJob(config.api_key, orgId, res.job_id);
25
- if (job.status === 'completed') {
26
- process.stdout.write('\r\x1b[K');
27
- if (flags.json)
28
- printJson(job);
29
- else
30
- success(`Image generated!\nURL: ${job.url}`);
31
- return;
32
- }
33
- if (job.status === 'failed') {
34
- process.stdout.write('\r\x1b[K');
35
- error(`Image generation failed: ${job.error || 'Unknown error'}`);
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
- process.stdout.write('\r\x1b[K');
42
- error("Image generation timed out");
72
+ success(`Image generated!\nID: ${final.job_id}\nURL: ${final.url}`);
43
73
  }
44
- export async function list(flags) {
74
+ async function list(flags) {
45
75
  const config = requireConfig();
46
- const orgId = flags.org || config.default_org;
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
- else
53
- printTable(images);
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
- export async function del(id, flags) {
87
+ async function get(id, flags) {
56
88
  const config = requireConfig();
57
- const orgId = flags.org || config.default_org;
58
- if (!orgId || !id) {
59
- error("Missing required arguments.\nUsage: myapi image delete <job_id> --org <id>\n(Or set defaults via: myapi config set-org <id>)");
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 deleted! (Job ${id} history retained, URL nullified)`);
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': 'myapi image generate --prompt "<text>" [--ratio <1:1|16:9>] [--style <style>] [--colors <hex_list>] [--org <id>]',
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 List all your generated images
76
- generate Generate a new AI image from a prompt ($0.05 per generation)
77
- delete Delete the physical image file for a job (history retained, URL nullified)
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
- info(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for the list.`);
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 a list of valid subcommands.`);
186
+ default: error(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for available subcommands.`);
95
187
  }
96
188
  }
@@ -1,4 +1,4 @@
1
- export declare function list(flags: Record<string, string | boolean>): Promise<void>;
2
- export declare function ingest(url: string, flags: Record<string, string | boolean>): Promise<void>;
3
- export declare function del(id: string, flags: Record<string, string | boolean>): Promise<void>;
4
- export declare function run(subcommand: string | undefined, args: string[], flags: Record<string, string | boolean>): Promise<void>;
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>;
@@ -1,39 +1,127 @@
1
- import { storage as sdkStorage } from '@myapihq/sdk';
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
- export async function list(flags) {
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.org || config.default_org;
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
- else
13
- printTable(assets);
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
- export async function ingest(url, flags) {
41
+ async function ingest(url, flags) {
16
42
  const config = requireConfig();
17
- const orgId = flags.org || config.default_org;
18
- if (!orgId || !url) {
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
- export async function del(id, flags) {
48
+ async function upload(filePath, flags) {
25
49
  const config = requireConfig();
26
- const orgId = flags.org || config.default_org;
27
- if (!orgId || !id) {
28
- error("Missing required arguments.\nUsage: myapi storage delete <id> --org <id>\n(Or set defaults via: myapi config set-org <id>)");
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': 'myapi storage ingest <url> [--name <name>] [--org <id>]',
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 List all your uploaded assets
45
- ingest Ingest a public image URL into your edge storage
46
- delete Delete a stored asset
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
- info(`Unknown subcommand: ${subcommand}. Run "myapi storage --help" for the list.`);
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 a list of valid subcommands.`);
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.stdout.write('\r\x1b[K');
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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "my-image-api",
3
+ "description": "Generate AI images from a text prompt. Async, polls until ready, returns a public CDN URL.",
4
+ "version": "1.0.0",
5
+ "published": true
6
+ }
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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "my-storage-api",
3
+ "description": "Edge-hosted asset storage. Upload local files or ingest from URLs, each gets a stable public CDN URL.",
4
+ "version": "1.0.0",
5
+ "published": true
6
+ }
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
- process.stdout.write(`${opts.label} `);
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
- process.stdout.write(`\r${opts.label} ${spinnerFrame(i++)}`);
39
+ spinnerWrite(`\r${opts.label} ${spinnerFrame(i++)}`);
40
40
  await sleep(intervalMs);
41
41
  elapsed += intervalMs;
42
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
- "version": "1.1.0-wip.3",
3
+ "version": "1.1.0-wip.4",
4
4
  "description": "MyAPI command-line interface",
5
5
  "type": "module",
6
6
  "files": [