memes-business 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # memes-business
2
+
3
+ CLI for [memes.business](https://memes.business) — generate memes from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g memes-business
9
+ ```
10
+
11
+ ## Authentication
12
+
13
+ Get your API key from [memes.business/settings](https://memes.business/settings), then:
14
+
15
+ ```bash
16
+ memes login --api-key <your-api-key>
17
+ ```
18
+
19
+ Verify you're logged in:
20
+
21
+ ```bash
22
+ memes whoami
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Generate Memes with AI
28
+
29
+ ```bash
30
+ memes generate --ai "make a meme about Monday meetings"
31
+ ```
32
+
33
+ Save the generated meme to a file:
34
+
35
+ ```bash
36
+ memes generate --ai "distracted boyfriend meme about coding vs debugging" --save meme.png
37
+ ```
38
+
39
+ ### Generate Memes Manually
40
+
41
+ Search for a template:
42
+
43
+ ```bash
44
+ memes search "drake"
45
+ ```
46
+
47
+ Generate with specific text:
48
+
49
+ ```bash
50
+ memes generate <template-id> --text "Top text" --text "Bottom text"
51
+ ```
52
+
53
+ ### Manage Your Memes
54
+
55
+ List your saved memes:
56
+
57
+ ```bash
58
+ memes library list
59
+ ```
60
+
61
+ Show details of a specific meme:
62
+
63
+ ```bash
64
+ memes library show <meme-id>
65
+ ```
66
+
67
+ Download a meme:
68
+
69
+ ```bash
70
+ memes library download <meme-id> ./my-meme.png
71
+ ```
72
+
73
+ ### Favorites
74
+
75
+ List your favorites:
76
+
77
+ ```bash
78
+ memes favorites
79
+ memes favorites --templates # only templates
80
+ memes favorites --memes # only memes
81
+ ```
82
+
83
+ Toggle favorite on a template:
84
+
85
+ ```bash
86
+ memes favorite <template-id>
87
+ ```
88
+
89
+ ### Account Status
90
+
91
+ Check your usage and subscription:
92
+
93
+ ```bash
94
+ memes status
95
+ ```
96
+
97
+ Open billing page:
98
+
99
+ ```bash
100
+ memes upgrade
101
+ ```
102
+
103
+ ## Commands
104
+
105
+ | Command | Description |
106
+ |---------|-------------|
107
+ | `memes login --api-key <key>` | Authenticate with your API key |
108
+ | `memes logout` | Remove stored credentials |
109
+ | `memes whoami` | Display current user info |
110
+ | `memes search <query>` | Search for meme templates |
111
+ | `memes generate --ai <prompt>` | Generate meme with AI |
112
+ | `memes generate <id> --text <text>` | Generate meme from template |
113
+ | `memes library list` | List your saved memes |
114
+ | `memes library show <id>` | Show meme details |
115
+ | `memes library download <id> [path]` | Download meme image |
116
+ | `memes favorites` | List favorited items |
117
+ | `memes favorite <template-id>` | Toggle favorite on template |
118
+ | `memes status` | Show subscription status |
119
+ | `memes upgrade` | Open billing page |
120
+
121
+ ## JSON Output
122
+
123
+ Most commands support `--json` for scripting:
124
+
125
+ ```bash
126
+ memes search "cat" --json | jq '.data[0].id'
127
+ memes status --json
128
+ memes whoami --json
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
134
+
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ export declare function createLoginCommand(): Command;
3
+ export declare function createLogoutCommand(): Command;
4
+ export declare function createWhoamiCommand(): Command;
@@ -0,0 +1,92 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { setApiKey, clearApiKey, getApiKey, CONFIG_DIR } from "../lib/config.js";
5
+ import { getCurrentSession, verifyApiKey } from "../lib/api.js";
6
+ export function createLoginCommand() {
7
+ const login = new Command("login")
8
+ .description("Authenticate with memes.business")
9
+ .option("--api-key <key>", "API key for authentication")
10
+ .action(async (options) => {
11
+ if (!options.apiKey) {
12
+ console.log(chalk.yellow("Please provide an API key:"));
13
+ console.log(chalk.dim(" memes login --api-key <your-api-key>"));
14
+ console.log();
15
+ console.log(chalk.dim("Get your API key at: https://memes.business/settings"));
16
+ process.exit(1);
17
+ }
18
+ const spinner = ora("Validating API key...").start();
19
+ const result = await verifyApiKey(options.apiKey);
20
+ if (result.error) {
21
+ spinner.fail(chalk.red(result.error.message));
22
+ process.exit(1);
23
+ }
24
+ await setApiKey(options.apiKey);
25
+ spinner.succeed(chalk.green("Logged in successfully!"));
26
+ console.log();
27
+ console.log(chalk.dim(` User: ${result.data?.user.name}`));
28
+ console.log(chalk.dim(` Email: ${result.data?.user.email}`));
29
+ console.log();
30
+ console.log(chalk.dim(`Credentials saved to ${CONFIG_DIR}`));
31
+ });
32
+ return login;
33
+ }
34
+ export function createLogoutCommand() {
35
+ const logout = new Command("logout")
36
+ .description("Clear stored credentials")
37
+ .action(async () => {
38
+ const apiKey = await getApiKey();
39
+ if (!apiKey) {
40
+ console.log(chalk.yellow("Not currently logged in."));
41
+ return;
42
+ }
43
+ const spinner = ora("Logging out...").start();
44
+ await clearApiKey();
45
+ spinner.succeed(chalk.green("Logged out successfully!"));
46
+ console.log(chalk.dim(`Credentials cleared from ${CONFIG_DIR}`));
47
+ });
48
+ return logout;
49
+ }
50
+ export function createWhoamiCommand() {
51
+ const whoami = new Command("whoami")
52
+ .description("Display current user info")
53
+ .option("--json", "Output as JSON")
54
+ .action(async (options) => {
55
+ const apiKey = await getApiKey();
56
+ if (!apiKey) {
57
+ if (options.json) {
58
+ console.log(JSON.stringify({ error: "Not logged in" }));
59
+ }
60
+ else {
61
+ console.log(chalk.yellow("Not logged in."));
62
+ console.log(chalk.dim("Run 'memes login --api-key <key>' to authenticate."));
63
+ }
64
+ process.exit(1);
65
+ }
66
+ const spinner = ora("Fetching user info...").start();
67
+ const result = await getCurrentSession();
68
+ if (result.error) {
69
+ spinner.fail(chalk.red(result.error.message));
70
+ if (options.json) {
71
+ console.log(JSON.stringify({ error: result.error }));
72
+ }
73
+ process.exit(1);
74
+ }
75
+ spinner.stop();
76
+ const user = result.data?.user;
77
+ if (options.json) {
78
+ console.log(JSON.stringify(user, null, 2));
79
+ return;
80
+ }
81
+ console.log(chalk.bold("Current User"));
82
+ console.log();
83
+ console.log(` ${chalk.dim("Name:")} ${user?.name}`);
84
+ console.log(` ${chalk.dim("Email:")} ${user?.email}`);
85
+ console.log(` ${chalk.dim("ID:")} ${user?.id}`);
86
+ if (user?.role) {
87
+ console.log(` ${chalk.dim("Role:")} ${user.role}`);
88
+ }
89
+ console.log(` ${chalk.dim("Verified:")} ${user?.emailVerified ? chalk.green("Yes") : chalk.yellow("No")}`);
90
+ });
91
+ return whoami;
92
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function createFavoritesCommand(): Command;
3
+ export declare function createFavoriteCommand(): Command;
@@ -0,0 +1,102 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { apiRequest } from "../lib/api.js";
5
+ export function createFavoritesCommand() {
6
+ const favorites = new Command("favorites")
7
+ .description("List favorited templates and memes")
8
+ .option("--templates", "show only templates")
9
+ .option("--memes", "show only memes")
10
+ .option("--json", "output as JSON for scripting")
11
+ .action(async (options) => {
12
+ // Check for mutually exclusive flags
13
+ if (options.templates && options.memes) {
14
+ const errorMessage = "Error: --templates and --memes are mutually exclusive. Use one or the other.";
15
+ if (options.json) {
16
+ console.log(JSON.stringify({ error: { message: errorMessage } }, null, 2));
17
+ }
18
+ else {
19
+ console.error(chalk.red(errorMessage));
20
+ }
21
+ process.exit(1);
22
+ }
23
+ const spinner = options.json ? null : ora("Fetching favorites...").start();
24
+ const params = new URLSearchParams();
25
+ if (options.templates)
26
+ params.set("entity_type", "template");
27
+ if (options.memes)
28
+ params.set("entity_type", "user_meme");
29
+ const queryString = params.toString();
30
+ const path = `/api/favorites/list${queryString ? `?${queryString}` : ""}`;
31
+ const result = await apiRequest(path);
32
+ if (result.error) {
33
+ if (spinner)
34
+ spinner.fail(chalk.red(result.error.message));
35
+ if (options.json) {
36
+ console.log(JSON.stringify({ error: result.error }, null, 2));
37
+ }
38
+ process.exit(1);
39
+ }
40
+ const response = result.data;
41
+ if (!response || !response.data) {
42
+ if (spinner)
43
+ spinner.fail(chalk.red("Invalid response from server"));
44
+ if (options.json) {
45
+ console.log(JSON.stringify({ error: { message: "Invalid response" } }, null, 2));
46
+ }
47
+ process.exit(1);
48
+ }
49
+ if (spinner)
50
+ spinner.stop();
51
+ if (options.json) {
52
+ console.log(JSON.stringify(response, null, 2));
53
+ return;
54
+ }
55
+ const items = response.data;
56
+ if (items.length === 0) {
57
+ const filter = options.templates ? "templates" : options.memes ? "memes" : "items";
58
+ console.log(chalk.yellow(`No favorited ${filter} found.`));
59
+ return;
60
+ }
61
+ console.log(chalk.bold(`${response.meta.total_count} favorite${response.meta.total_count !== 1 ? "s" : ""}`));
62
+ console.log();
63
+ items.forEach((item, index) => {
64
+ const num = chalk.dim(`${(index + 1).toString().padStart(2)}.`);
65
+ const name = chalk.bold(item.name);
66
+ const type = chalk.cyan(`[${item.type}]`);
67
+ console.log(`${num} ${name} ${type}`);
68
+ console.log(chalk.dim(` ID: ${item.id}`));
69
+ console.log();
70
+ });
71
+ });
72
+ return favorites;
73
+ }
74
+ export function createFavoriteCommand() {
75
+ const favorite = new Command("favorite")
76
+ .description("Toggle favorite status on a template")
77
+ .argument("<template-id>", "template ID to favorite/unfavorite")
78
+ .action(async (templateId) => {
79
+ const spinner = ora("Toggling favorite...").start();
80
+ const result = await apiRequest("/api/favorites", {
81
+ method: "POST",
82
+ body: JSON.stringify({ entity_type: "template", entity_id: templateId }),
83
+ });
84
+ if (result.error) {
85
+ spinner.fail(chalk.red(result.error.message));
86
+ process.exit(1);
87
+ }
88
+ const response = result.data;
89
+ if (!response || !response.data) {
90
+ spinner.fail(chalk.red("Invalid response from server"));
91
+ process.exit(1);
92
+ }
93
+ const { isFavorited, message } = response.data;
94
+ if (isFavorited) {
95
+ spinner.succeed(chalk.green(message || `Added template ${templateId} to favorites`));
96
+ }
97
+ else {
98
+ spinner.succeed(chalk.yellow(message || `Removed template ${templateId} from favorites`));
99
+ }
100
+ });
101
+ return favorite;
102
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createGenerateCommand(): Command;
@@ -0,0 +1,312 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { writeFile, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { apiRequest } from "../lib/api.js";
7
+ import { getApiKey, getApiUrl } from "../lib/config.js";
8
+ /**
9
+ * Make a streaming request for AI generation
10
+ */
11
+ async function streamAIGeneration(prompt, onMeme, onError) {
12
+ const baseUrl = await getApiUrl();
13
+ const apiKey = await getApiKey();
14
+ if (!apiKey) {
15
+ throw new Error("Not authenticated. Run 'memes login' first.");
16
+ }
17
+ const response = await fetch(`${baseUrl}/api/generate`, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ "x-api-key": apiKey,
22
+ },
23
+ body: JSON.stringify({ request: prompt }),
24
+ });
25
+ if (!response.ok) {
26
+ const errorData = await response.json().catch(() => ({}));
27
+ throw new Error(errorData.error?.message ||
28
+ `HTTP ${response.status}: ${response.statusText}`);
29
+ }
30
+ const reader = response.body?.getReader();
31
+ if (!reader) {
32
+ throw new Error("No response body");
33
+ }
34
+ const decoder = new TextDecoder();
35
+ let buffer = "";
36
+ while (true) {
37
+ const { done, value } = await reader.read();
38
+ if (done)
39
+ break;
40
+ buffer += decoder.decode(value, { stream: true });
41
+ const lines = buffer.split("\n");
42
+ buffer = lines.pop() || "";
43
+ for (const line of lines) {
44
+ if (!line.trim())
45
+ continue;
46
+ try {
47
+ let jsonStr;
48
+ // Handle server's numbered format: "0: {...}"
49
+ const numberedMatch = line.match(/^\d+:\s*(.+)$/);
50
+ if (numberedMatch) {
51
+ jsonStr = numberedMatch[1];
52
+ }
53
+ else if (line.startsWith("data:")) {
54
+ // Handle SSE format: "data: {...}"
55
+ jsonStr = line.slice(5).trim();
56
+ }
57
+ else {
58
+ continue;
59
+ }
60
+ if (!jsonStr || jsonStr === "[DONE]")
61
+ continue;
62
+ const event = JSON.parse(jsonStr);
63
+ // Handle server's generatedMeme format
64
+ if (event.generatedMeme) {
65
+ const meme = event.generatedMeme;
66
+ onMeme({
67
+ name: meme.title || meme.template_name || "Meme",
68
+ texts: meme.generation_data?.texts || [],
69
+ imageUrl: `${baseUrl}/api/memes/${meme.id}/image`,
70
+ templateId: meme.generation_data?.template_id,
71
+ memeId: meme.id,
72
+ });
73
+ }
74
+ // Handle legacy SSE format
75
+ else if (event.type === "meme" && (event.data || event.meme)) {
76
+ onMeme(event.data || event.meme);
77
+ }
78
+ else if (event.type === "error" || event.error) {
79
+ onError(event.error || "Unknown error");
80
+ }
81
+ }
82
+ catch {
83
+ // Skip unparseable lines
84
+ }
85
+ }
86
+ }
87
+ // Process any remaining content in buffer after stream ends
88
+ if (buffer.trim()) {
89
+ try {
90
+ let jsonStr;
91
+ // Handle server's numbered format: "0: {...}"
92
+ const numberedMatch = buffer.match(/^\d+:\s*(.+)$/);
93
+ if (numberedMatch) {
94
+ jsonStr = numberedMatch[1];
95
+ }
96
+ else if (buffer.startsWith("data:")) {
97
+ // Handle SSE format: "data: {...}"
98
+ jsonStr = buffer.slice(5).trim();
99
+ }
100
+ else {
101
+ jsonStr = "";
102
+ }
103
+ if (jsonStr && jsonStr !== "[DONE]") {
104
+ const event = JSON.parse(jsonStr);
105
+ // Handle server's generatedMeme format
106
+ if (event.generatedMeme) {
107
+ const meme = event.generatedMeme;
108
+ onMeme({
109
+ name: meme.title || meme.template_name || "Meme",
110
+ texts: meme.generation_data?.texts || [],
111
+ imageUrl: `${baseUrl}/api/memes/${meme.id}/image`,
112
+ templateId: meme.generation_data?.template_id,
113
+ memeId: meme.id,
114
+ });
115
+ }
116
+ // Handle legacy SSE format
117
+ else if (event.type === "meme" && (event.data || event.meme)) {
118
+ onMeme(event.data || event.meme);
119
+ }
120
+ else if (event.type === "error" || event.error) {
121
+ onError(event.error || "Unknown error");
122
+ }
123
+ }
124
+ }
125
+ catch {
126
+ // Skip unparseable content
127
+ }
128
+ }
129
+ }
130
+ /**
131
+ * Check if a path is a directory
132
+ */
133
+ async function isDirectory(path) {
134
+ try {
135
+ const stats = await stat(path);
136
+ return stats.isDirectory();
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ /**
143
+ * Get file extension from content type
144
+ */
145
+ function getExtensionFromContentType(contentType) {
146
+ if (!contentType)
147
+ return ".jpg";
148
+ if (contentType.includes("png"))
149
+ return ".png";
150
+ if (contentType.includes("gif"))
151
+ return ".gif";
152
+ if (contentType.includes("webp"))
153
+ return ".webp";
154
+ return ".jpg";
155
+ }
156
+ /**
157
+ * Download an image from URL
158
+ */
159
+ async function downloadImage(imageUrl, savePath, memeId) {
160
+ const baseUrl = await getApiUrl();
161
+ const apiKey = await getApiKey();
162
+ // Handle relative URLs
163
+ const fullUrl = imageUrl.startsWith("http")
164
+ ? imageUrl
165
+ : `${baseUrl}${imageUrl}`;
166
+ const response = await fetch(fullUrl, {
167
+ headers: apiKey ? { "x-api-key": apiKey } : {},
168
+ });
169
+ if (!response.ok) {
170
+ throw new Error(`Failed to download: ${response.statusText}`);
171
+ }
172
+ const contentType = response.headers.get("content-type");
173
+ const ext = getExtensionFromContentType(contentType);
174
+ // Resolve the save path (handle directory case)
175
+ let resolvedPath = savePath;
176
+ if (await isDirectory(savePath)) {
177
+ const filename = memeId ? `${memeId}${ext}` : `meme-${Date.now()}${ext}`;
178
+ resolvedPath = join(savePath, filename);
179
+ }
180
+ const buffer = await response.arrayBuffer();
181
+ await writeFile(resolvedPath, Buffer.from(buffer));
182
+ return resolvedPath;
183
+ }
184
+ export function createGenerateCommand() {
185
+ const generate = new Command("generate")
186
+ .description("Generate a meme from a template or AI prompt")
187
+ .argument("[template]", "template ID (for manual generation)")
188
+ .option("-t, --text <text...>", "text for each text box (manual mode)")
189
+ .option("-a, --ai <prompt>", "AI prompt for generation")
190
+ .option("-s, --save <path>", "save the meme image to a file")
191
+ .action(async (template, options) => {
192
+ const { text, ai, save } = options;
193
+ // Validate arguments
194
+ if (!template && !ai) {
195
+ console.log(chalk.red("Error: Provide a template ID or use --ai"));
196
+ console.log();
197
+ console.log(chalk.dim("Manual: memes generate <template-id> --text \"Top\" --text \"Bottom\""));
198
+ console.log(chalk.dim("AI: memes generate --ai \"make a meme about Monday\""));
199
+ process.exit(1);
200
+ }
201
+ if (template && ai) {
202
+ console.log(chalk.red("Error: Cannot use both template and --ai"));
203
+ process.exit(1);
204
+ }
205
+ if (ai) {
206
+ await handleAIGeneration(ai, save);
207
+ }
208
+ else {
209
+ await handleManualGeneration(template, text || [], save);
210
+ }
211
+ });
212
+ return generate;
213
+ }
214
+ async function handleManualGeneration(templateId, texts, savePath) {
215
+ if (texts.length === 0) {
216
+ console.log(chalk.red("Error: At least one --text is required"));
217
+ process.exit(1);
218
+ }
219
+ const spinner = ora("Generating meme...").start();
220
+ const result = await apiRequest("/api/memes", {
221
+ method: "POST",
222
+ body: JSON.stringify({
223
+ templateId,
224
+ texts,
225
+ }),
226
+ });
227
+ if (result.error) {
228
+ spinner.fail(chalk.red(result.error.message));
229
+ process.exit(1);
230
+ }
231
+ const meme = result.data?.data;
232
+ if (!meme) {
233
+ spinner.fail(chalk.red("No meme data in response"));
234
+ process.exit(1);
235
+ }
236
+ spinner.succeed(chalk.green("Meme generated!"));
237
+ console.log();
238
+ console.log(` ${chalk.dim("Title:")} ${meme.title}`);
239
+ console.log(` ${chalk.dim("URL:")} ${meme.imageUrl}`);
240
+ if (savePath) {
241
+ const saveSpinner = ora("Saving image...").start();
242
+ try {
243
+ const resolvedPath = await downloadImage(meme.imageUrl, savePath, meme.id);
244
+ saveSpinner.succeed(chalk.green(`Saved to ${resolvedPath}`));
245
+ }
246
+ catch (err) {
247
+ const msg = err instanceof Error ? err.message : "Unknown error";
248
+ saveSpinner.fail(chalk.red(`Failed to save: ${msg}`));
249
+ }
250
+ }
251
+ }
252
+ async function handleAIGeneration(prompt, savePath) {
253
+ const spinner = ora("Generating meme with AI...").start();
254
+ const memes = [];
255
+ let hasError = false;
256
+ let errorMsg = "";
257
+ try {
258
+ await streamAIGeneration(prompt, (meme) => {
259
+ memes.push(meme);
260
+ spinner.text = `Generated ${memes.length} meme(s)...`;
261
+ }, (error) => {
262
+ hasError = true;
263
+ errorMsg = error;
264
+ });
265
+ }
266
+ catch (err) {
267
+ spinner.fail(chalk.red(err instanceof Error ? err.message : "Generation failed"));
268
+ process.exit(1);
269
+ }
270
+ if (hasError) {
271
+ spinner.fail(chalk.red(errorMsg));
272
+ process.exit(1);
273
+ }
274
+ if (memes.length === 0) {
275
+ spinner.fail(chalk.red("No memes generated"));
276
+ process.exit(1);
277
+ }
278
+ spinner.succeed(chalk.green(`Generated ${memes.length} meme(s)!`));
279
+ console.log();
280
+ for (const meme of memes) {
281
+ console.log(` ${chalk.bold(meme.name)}`);
282
+ console.log(` ${chalk.dim("Texts:")} ${meme.texts.join(" | ")}`);
283
+ console.log(` ${chalk.dim("URL:")} ${meme.imageUrl}`);
284
+ console.log();
285
+ }
286
+ // Save memes if --save is provided
287
+ if (savePath && memes.length > 0) {
288
+ const isDir = await isDirectory(savePath);
289
+ const memesToSave = isDir ? memes : [memes[0]];
290
+ const saveSpinner = ora(`Saving ${memesToSave.length} image(s)...`).start();
291
+ const savedPaths = [];
292
+ const errors = [];
293
+ for (const meme of memesToSave) {
294
+ try {
295
+ const resolvedPath = await downloadImage(meme.imageUrl, savePath, meme.memeId);
296
+ savedPaths.push(resolvedPath);
297
+ }
298
+ catch (err) {
299
+ const msg = err instanceof Error ? err.message : "Unknown error";
300
+ errors.push(`${meme.name}: ${msg}`);
301
+ }
302
+ }
303
+ if (savedPaths.length > 0) {
304
+ saveSpinner.succeed(chalk.green(`Saved ${savedPaths.length} image(s) to ${isDir ? savePath : savedPaths[0]}`));
305
+ }
306
+ if (errors.length > 0) {
307
+ for (const error of errors) {
308
+ console.log(chalk.red(` ✖ ${error}`));
309
+ }
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createMemesCommand(): Command;