@thenavidm/midjourney-mcp-cli 1.0.0

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +677 -0
  3. package/SKILL.md +184 -0
  4. package/dist/api/client.d.ts +72 -0
  5. package/dist/api/client.js +278 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/download.d.ts +41 -0
  8. package/dist/api/download.js +108 -0
  9. package/dist/api/download.js.map +1 -0
  10. package/dist/api/errors.d.ts +75 -0
  11. package/dist/api/errors.js +166 -0
  12. package/dist/api/errors.js.map +1 -0
  13. package/dist/api/jobs.d.ts +140 -0
  14. package/dist/api/jobs.js +296 -0
  15. package/dist/api/jobs.js.map +1 -0
  16. package/dist/api/moodboards.d.ts +88 -0
  17. package/dist/api/moodboards.js +189 -0
  18. package/dist/api/moodboards.js.map +1 -0
  19. package/dist/capture.d.ts +27 -0
  20. package/dist/capture.js +162 -0
  21. package/dist/capture.js.map +1 -0
  22. package/dist/cli.d.ts +92 -0
  23. package/dist/cli.js +633 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +37 -0
  26. package/dist/config.js +90 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/content/prompt.d.ts +69 -0
  29. package/dist/content/prompt.js +173 -0
  30. package/dist/content/prompt.js.map +1 -0
  31. package/dist/doctor.d.ts +19 -0
  32. package/dist/doctor.js +161 -0
  33. package/dist/doctor.js.map +1 -0
  34. package/dist/format/jobs.d.ts +71 -0
  35. package/dist/format/jobs.js +211 -0
  36. package/dist/format/jobs.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.js +161 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/safety.d.ts +52 -0
  41. package/dist/safety.js +100 -0
  42. package/dist/safety.js.map +1 -0
  43. package/dist/server.d.ts +10 -0
  44. package/dist/server.js +57 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/tools/create.d.ts +96 -0
  47. package/dist/tools/create.js +375 -0
  48. package/dist/tools/create.js.map +1 -0
  49. package/dist/tools/download.d.ts +17 -0
  50. package/dist/tools/download.js +47 -0
  51. package/dist/tools/download.js.map +1 -0
  52. package/dist/tools/explore.d.ts +8 -0
  53. package/dist/tools/explore.js +57 -0
  54. package/dist/tools/explore.js.map +1 -0
  55. package/dist/tools/index.d.ts +3 -0
  56. package/dist/tools/index.js +16 -0
  57. package/dist/tools/index.js.map +1 -0
  58. package/dist/tools/jobs.d.ts +18 -0
  59. package/dist/tools/jobs.js +92 -0
  60. package/dist/tools/jobs.js.map +1 -0
  61. package/dist/tools/kit.d.ts +84 -0
  62. package/dist/tools/kit.js +104 -0
  63. package/dist/tools/kit.js.map +1 -0
  64. package/dist/tools/library.d.ts +22 -0
  65. package/dist/tools/library.js +185 -0
  66. package/dist/tools/library.js.map +1 -0
  67. package/dist/tools/profile.d.ts +2 -0
  68. package/dist/tools/profile.js +89 -0
  69. package/dist/tools/profile.js.map +1 -0
  70. package/dist/transport/cdp.d.ts +217 -0
  71. package/dist/transport/cdp.js +607 -0
  72. package/dist/transport/cdp.js.map +1 -0
  73. package/dist/transport/http.d.ts +18 -0
  74. package/dist/transport/http.js +48 -0
  75. package/dist/transport/http.js.map +1 -0
  76. package/package.json +72 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Submitting work and following it to the end.
3
+ *
4
+ * This is the part the reference CLI for this API does not have, and it is the
5
+ * part that makes the difference between a toy and something an agent can use.
6
+ * Submitting returns a job id in about a second; the image does not exist for
7
+ * another thirty to ninety. A tool that returns the id and stops has handed the
8
+ * caller a polling loop to write, and a model asked to "make me a logo" will
9
+ * either return an id nobody can use or invent a wait and guess wrong.
10
+ *
11
+ * So submission and completion are one operation here, with the polling,
12
+ * back-off and terminal-state detection on this side of the boundary.
13
+ */
14
+ import { JobTimeoutError, MidjourneyError, ValidationError } from "./errors.js";
15
+ import { extractJobs, isTerminal } from "../format/jobs.js";
16
+ export const ENDPOINTS = {
17
+ submit: "/api/submit-jobs",
18
+ jobs: "/api/imagine",
19
+ updates: "/api/imagine-update",
20
+ queue: "/api/user-queue",
21
+ folders: "/api/folders",
22
+ moodboards: "/api/moodboards",
23
+ storage: "/api/storage",
24
+ explore: "/api/explore",
25
+ exploreStyleLikes: "/api/explore-styles-likes",
26
+ personalizedProfiles: "/api/personalized-profiles",
27
+ following: "/api/following-for-user",
28
+ modelRatings: "/api/model-ratings",
29
+ contestsRankingCount: "/api/contests-ranking-count",
30
+ jobStatus: "/api/job-status",
31
+ };
32
+ /** `singleplayer_<uuid>` is what the web app calls a solo user's own channel. */
33
+ export function channelIdFor(userId) {
34
+ const trimmed = userId.trim();
35
+ return trimmed.startsWith("singleplayer_") ? trimmed : `singleplayer_${trimmed}`;
36
+ }
37
+ function metadataFor(options) {
38
+ return {
39
+ isMobile: null,
40
+ imagePrompts: options.imagePromptCount ?? 0,
41
+ imageReferences: options.styleRefCount ?? 0,
42
+ characterReferences: options.omniRefCount ?? 0,
43
+ depthReferences: 0,
44
+ lightboxOpen: null,
45
+ };
46
+ }
47
+ /** Job ids out of a submission response, whatever it is wrapped in. */
48
+ export function extractJobIds(payload, depth = 0) {
49
+ if (depth > 5 || payload === null || payload === undefined)
50
+ return [];
51
+ if (typeof payload === "string") {
52
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(payload)
53
+ ? [payload]
54
+ : [];
55
+ }
56
+ if (Array.isArray(payload)) {
57
+ return payload.flatMap((item) => extractJobIds(item, depth + 1));
58
+ }
59
+ if (typeof payload !== "object")
60
+ return [];
61
+ const record = payload;
62
+ const found = [];
63
+ for (const key of ["job_id", "jobId", "id", "success_jobs", "jobs"]) {
64
+ if (key in record)
65
+ found.push(...extractJobIds(record[key], depth + 1));
66
+ }
67
+ if (found.length === 0) {
68
+ for (const value of Object.values(record))
69
+ found.push(...extractJobIds(value, depth + 1));
70
+ }
71
+ return [...new Set(found)];
72
+ }
73
+ /** Submit an imagine. Returns the job ids Midjourney accepted. */
74
+ export async function submitImagine(client, prompt, options = {}) {
75
+ const userId = await client.userId();
76
+ const body = {
77
+ f: { mode: options.speed ?? client.config.defaultSpeed, private: options.private ?? false },
78
+ channelId: channelIdFor(userId),
79
+ metadata: metadataFor(options),
80
+ t: "imagine",
81
+ prompt,
82
+ };
83
+ // Never retried. A retry after a timeout would submit a second job and charge
84
+ // for it, and there is no way from here to tell a lost response from a lost
85
+ // request.
86
+ const raw = await client.request(ENDPOINTS.submit, {
87
+ method: "POST",
88
+ body,
89
+ noRetry: true,
90
+ });
91
+ refreshView(client, options.refresh !== false);
92
+ return { jobIds: extractJobIds(raw), raw };
93
+ }
94
+ /**
95
+ * Nudge the open window, without ever letting it affect the result.
96
+ *
97
+ * Deliberately not awaited: the generation has already been paid for by the
98
+ * time this runs, and a cosmetic reload must not be able to turn a successful
99
+ * submission into a thrown error.
100
+ */
101
+ function refreshView(client, enabled = true) {
102
+ if (!enabled || !client.config.refreshView)
103
+ return;
104
+ void client.transport.refreshView().catch(() => undefined);
105
+ }
106
+ /** Re-run an existing job, unchanged or with a new prompt. */
107
+ export async function submitRerun(client, jobId, options = {}) {
108
+ const userId = await client.userId();
109
+ const body = {
110
+ f: { mode: options.speed ?? client.config.defaultSpeed, private: options.private ?? false },
111
+ channelId: channelIdFor(userId),
112
+ metadata: metadataFor(options),
113
+ t: "reroll",
114
+ newPrompt: options.newPrompt ?? null,
115
+ id: jobId.trim(),
116
+ };
117
+ const raw = await client.request(ENDPOINTS.submit, {
118
+ method: "POST",
119
+ body,
120
+ noRetry: true,
121
+ });
122
+ refreshView(client, options.refresh !== false);
123
+ return { jobIds: extractJobIds(raw), raw };
124
+ }
125
+ /**
126
+ * Vary one image from a finished grid.
127
+ *
128
+ * Captured from the web app rather than guessed: `Vary Subtle` and `Vary
129
+ * Strong` are the same job type with a `strong` boolean, addressing one tile by
130
+ * `index`. Note the metadata block here is all nulls, where an imagine sends
131
+ * counts. That difference is what the app sends, so it is what we send.
132
+ */
133
+ export async function submitVary(client, jobId, index, options = {}) {
134
+ const userId = await client.userId();
135
+ const body = {
136
+ f: { mode: options.speed ?? client.config.defaultSpeed, private: options.private ?? false },
137
+ channelId: channelIdFor(userId),
138
+ metadata: {
139
+ isMobile: null,
140
+ imagePrompts: null,
141
+ imageReferences: null,
142
+ characterReferences: null,
143
+ depthReferences: null,
144
+ lightboxOpen: null,
145
+ },
146
+ t: "vary",
147
+ strong: options.strong ?? false,
148
+ v8_1_hd: null,
149
+ v8_2_hd: null,
150
+ id: jobId.trim(),
151
+ index,
152
+ };
153
+ const raw = await client.request(ENDPOINTS.submit, {
154
+ method: "POST",
155
+ body,
156
+ noRetry: true,
157
+ });
158
+ refreshView(client, options.refresh !== false);
159
+ return { jobIds: extractJobIds(raw), raw };
160
+ }
161
+ /**
162
+ * Submit an arbitrary job type.
163
+ *
164
+ * The escape hatch, and deliberately not dressed up as anything else. Only
165
+ * `imagine` and `reroll` are confirmed against observed traffic. The web app
166
+ * sends other values of `t` for upscales and variations, but guessing at their
167
+ * payloads and shipping them as named tools would mean charging the user for
168
+ * requests that quietly do nothing. Capture the real traffic first:
169
+ * `midjourney-cli capture` records what the app actually sends.
170
+ */
171
+ export async function submitRaw(client, jobType, extra, options = {}) {
172
+ if (!jobType.trim())
173
+ throw new ValidationError("A job type is required.", 0, "(local)");
174
+ const userId = await client.userId();
175
+ const body = {
176
+ f: { mode: options.speed ?? client.config.defaultSpeed, private: options.private ?? false },
177
+ channelId: channelIdFor(userId),
178
+ metadata: metadataFor(options),
179
+ t: jobType.trim(),
180
+ ...extra,
181
+ };
182
+ const raw = await client.request(ENDPOINTS.submit, {
183
+ method: "POST",
184
+ body,
185
+ noRetry: true,
186
+ });
187
+ refreshView(client, options.refresh !== false);
188
+ return { jobIds: extractJobIds(raw), raw };
189
+ }
190
+ /** Recent jobs for the signed-in account. */
191
+ export async function listJobs(client, options = {}) {
192
+ const userId = options.userId ?? (await client.userId());
193
+ const raw = await client.request(ENDPOINTS.jobs, {
194
+ query: {
195
+ user_id: userId.replace(/^singleplayer_/, ""),
196
+ page_size: options.limit ?? 25,
197
+ cursor: options.cursor,
198
+ },
199
+ });
200
+ return { jobs: extractJobs(raw), raw };
201
+ }
202
+ /** The update feed, which is what the web app polls while work is running. */
203
+ export async function jobUpdates(client, options = {}) {
204
+ const userId = options.userId ?? (await client.userId());
205
+ const raw = await client.request(ENDPOINTS.updates, {
206
+ query: {
207
+ user_id: userId.replace(/^singleplayer_/, ""),
208
+ page_size: options.limit ?? 25,
209
+ checkpoint: options.checkpoint,
210
+ },
211
+ });
212
+ return { jobs: extractJobs(raw), raw };
213
+ }
214
+ /**
215
+ * Ask about specific jobs by id.
216
+ *
217
+ * The one endpoint that answers "is this done?" honestly. It carries
218
+ * `current_status`, and it works for a job that has not reached the history
219
+ * feed yet, which is every job while it is still rendering.
220
+ *
221
+ * Everything else here was built before this was found, by inferring completion
222
+ * from whether image URLs could be derived. That inference happened to be right
223
+ * because the history feed only lists finished work, but it could never have
224
+ * reported "running" for anything.
225
+ */
226
+ export async function jobStatus(client, jobIds) {
227
+ if (jobIds.length === 0)
228
+ return [];
229
+ const raw = await client.request(ENDPOINTS.jobStatus, {
230
+ method: "POST",
231
+ body: { jobIds, _frontend_source: "midjourney-mcp" },
232
+ });
233
+ return extractJobs(raw);
234
+ }
235
+ /** One job by id, asking the status endpoint first. */
236
+ export async function findJob(client, jobId) {
237
+ const id = jobId.trim();
238
+ const direct = await jobStatus(client, [id]).catch(() => []);
239
+ const found = direct.find((job) => job.id === id);
240
+ if (found)
241
+ return found;
242
+ // Fallbacks, for a job the status endpoint does not know: it may have been
243
+ // submitted a moment ago, or belong to an older model line.
244
+ const fromUpdates = await jobUpdates(client, { limit: 50 }).catch(() => undefined);
245
+ const inUpdates = fromUpdates?.jobs.find((job) => job.id === id);
246
+ if (inUpdates)
247
+ return inUpdates;
248
+ const fromList = await listJobs(client, { limit: 50 }).catch(() => undefined);
249
+ return fromList?.jobs.find((job) => job.id === id);
250
+ }
251
+ /**
252
+ * Poll until a job reaches a terminal state.
253
+ *
254
+ * The interval widens as the wait goes on. A generation is quick at fast speed
255
+ * and can be twenty minutes at relax, and polling every three seconds for
256
+ * twenty minutes is four hundred requests nobody needs, on an endpoint we would
257
+ * rather not be conspicuous on.
258
+ */
259
+ export async function waitForJob(client, jobId, options = {}) {
260
+ const timeoutMs = options.timeoutMs ?? client.config.jobTimeoutMs;
261
+ const baseInterval = options.pollIntervalMs ?? client.config.jobPollIntervalMs;
262
+ const startedAt = Date.now();
263
+ for (let poll = 0;; poll++) {
264
+ const elapsed = Date.now() - startedAt;
265
+ if (elapsed > timeoutMs) {
266
+ throw new JobTimeoutError(`Job ${jobId} had not finished after ${Math.round(timeoutMs / 1000)}s. It may still be running: check with get_job. Relax-mode jobs regularly take longer than the default timeout, so raise MIDJOURNEY_JOB_TIMEOUT_MS if this is normal for your plan.`, jobId);
267
+ }
268
+ const job = await findJob(client, jobId).catch((error) => {
269
+ // A single failed poll is not a failed job. Keep waiting unless the
270
+ // failure is one that will repeat forever.
271
+ if (error instanceof MidjourneyError && ["NotSignedInError", "WriteBlockedError"].includes(error.name)) {
272
+ throw error;
273
+ }
274
+ return undefined;
275
+ });
276
+ options.onProgress?.(job, elapsed);
277
+ if (job && isTerminal(job.status)) {
278
+ // Again on completion: the reload at submit time showed an empty slot,
279
+ // this one shows the finished images.
280
+ refreshView(client);
281
+ return job;
282
+ }
283
+ // 3s, 3s, 4.5s, 6.75s ... capped at 20s.
284
+ const interval = Math.min(baseInterval * Math.pow(1.5, Math.max(0, poll - 1)), 20_000);
285
+ await new Promise((resolve) => setTimeout(resolve, interval));
286
+ // No early bail-out for a job that has not appeared yet.
287
+ //
288
+ // `/api/imagine` is a history feed: a job shows up in it once it has
289
+ // finished, not while it renders. So "not in the feed" is the normal state
290
+ // for the entire render, and giving up after a handful of polls would fail
291
+ // every relax-mode job, which is exactly when waiting matters most.
292
+ // Verified: a fast v8.1 job is absent at t+5s and present, complete, at
293
+ // t+10s. The only honest deadline is the timeout the caller asked for.
294
+ }
295
+ }
296
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/api/jobs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Moodboards and personalisation, as things a prompt can actually use.
3
+ *
4
+ * A moodboard in the web app is a named pile of reference images. The API hands
5
+ * back each image as an `s.mj.run` URL, which is the same form `--sref` takes,
6
+ * so a moodboard is already a style reference, it just has to be looked up by
7
+ * the name a person calls it rather than a nineteen-digit id.
8
+ *
9
+ * That lookup is the point of this file. "Use my High Fashion moodboard" is how
10
+ * someone thinks about it; `--sref https://s.mj.run/gJFLgT-zAvg --sref ...` is
11
+ * what Midjourney needs, and nobody should be pasting those by hand.
12
+ */
13
+ import type { MidjourneyClient } from "./client.js";
14
+ export type MoodboardImage = {
15
+ url: string;
16
+ width?: number;
17
+ height?: number;
18
+ };
19
+ export type Moodboard = {
20
+ id: string;
21
+ title: string;
22
+ personalize: boolean;
23
+ created?: string;
24
+ images: MoodboardImage[];
25
+ };
26
+ export type PersonalizationProfile = {
27
+ id: string;
28
+ title: string;
29
+ rankingCount?: number;
30
+ majorVersion?: string;
31
+ };
32
+ export declare function listMoodboards(client: MidjourneyClient): Promise<Moodboard[]>;
33
+ export declare function listProfiles(client: MidjourneyClient): Promise<PersonalizationProfile[]>;
34
+ /**
35
+ * Find a moodboard by the name someone would say, or by id.
36
+ *
37
+ * Exact title first, then case-insensitive, then a unique substring. An
38
+ * ambiguous substring is an error naming the candidates rather than a silent
39
+ * pick: choosing "Porsche Campaign" when someone meant "Mercedes Campaign"
40
+ * would be wrong in a way that costs a generation to discover.
41
+ */
42
+ export declare function matchMoodboard(boards: Moodboard[], query: string): Moodboard;
43
+ /**
44
+ * Pick references out of a moodboard.
45
+ *
46
+ * Every image would make an unusable prompt: a 242-image board becomes 242
47
+ * `--sref` flags. A handful is what the web app effectively does too, and
48
+ * spreading the pick across the board rather than taking the first few avoids
49
+ * always drawing on whatever was added first.
50
+ */
51
+ export declare function pickReferences(board: Moodboard, count: number): string[];
52
+ /** Resolve "use my High Fashion moodboard" to the style references it means. */
53
+ export declare function referencesFromMoodboard(client: MidjourneyClient, query: string, count: number): Promise<{
54
+ board: Moodboard;
55
+ refs: string[];
56
+ }>;
57
+ /**
58
+ * The write API, captured from the web app rather than guessed at.
59
+ *
60
+ * Worth writing down how it actually works, because none of it is guessable.
61
+ * Creating is a POST to the collection. Adding is a PATCH to the *collection*
62
+ * with the board in a query parameter, not a POST to the board, and the body
63
+ * carries an `add` array of image records rather than bare URLs. Every shape I
64
+ * tried before capturing the real call returned a 500 that said nothing.
65
+ *
66
+ * The web app also uploads the image into the account's own storage first, via
67
+ * `/api/storage-upload-moodboard-file`, and adds the re-hosted copy. That step
68
+ * turns out to be skippable: a `cdn.midjourney.com` job URL patches in directly
69
+ * and the board renders it. Verified against a real board.
70
+ */
71
+ export type AddImage = {
72
+ url: string;
73
+ width?: number;
74
+ height?: number;
75
+ };
76
+ export declare function createMoodboard(client: MidjourneyClient, title: string): Promise<{
77
+ id?: string;
78
+ title: string;
79
+ raw: unknown;
80
+ }>;
81
+ export declare function addToMoodboard(client: MidjourneyClient, moodboardId: string, images: AddImage[]): Promise<{
82
+ added: number;
83
+ raw: unknown;
84
+ }>;
85
+ export declare function removeFromMoodboard(client: MidjourneyClient, moodboardId: string, urls: string[]): Promise<{
86
+ removed: number;
87
+ raw: unknown;
88
+ }>;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Moodboards and personalisation, as things a prompt can actually use.
3
+ *
4
+ * A moodboard in the web app is a named pile of reference images. The API hands
5
+ * back each image as an `s.mj.run` URL, which is the same form `--sref` takes,
6
+ * so a moodboard is already a style reference, it just has to be looked up by
7
+ * the name a person calls it rather than a nineteen-digit id.
8
+ *
9
+ * That lookup is the point of this file. "Use my High Fashion moodboard" is how
10
+ * someone thinks about it; `--sref https://s.mj.run/gJFLgT-zAvg --sref ...` is
11
+ * what Midjourney needs, and nobody should be pasting those by hand.
12
+ */
13
+ import { NotFoundError } from "./errors.js";
14
+ import { ENDPOINTS } from "./jobs.js";
15
+ function asArray(payload) {
16
+ if (Array.isArray(payload))
17
+ return payload.filter((item) => !!item && typeof item === "object");
18
+ if (payload && typeof payload === "object") {
19
+ for (const key of ["moodboards", "profiles", "data", "results", "items"]) {
20
+ const nested = payload[key];
21
+ if (Array.isArray(nested))
22
+ return asArray(nested);
23
+ }
24
+ }
25
+ return [];
26
+ }
27
+ function normaliseMoodboard(record) {
28
+ const id = record.moodboard_id ?? record.id;
29
+ if (typeof id !== "string" && typeof id !== "number")
30
+ return undefined;
31
+ const rawImages = Array.isArray(record.images) ? record.images : [];
32
+ const images = [];
33
+ for (const item of rawImages) {
34
+ if (!item || typeof item !== "object")
35
+ continue;
36
+ const url = item.url;
37
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url))
38
+ continue;
39
+ const width = item.width;
40
+ const height = item.height;
41
+ images.push({
42
+ url,
43
+ ...(typeof width === "number" ? { width } : {}),
44
+ ...(typeof height === "number" ? { height } : {}),
45
+ });
46
+ }
47
+ return {
48
+ id: String(id),
49
+ title: typeof record.title === "string" ? record.title : "(untitled)",
50
+ personalize: record.personalize === true,
51
+ ...(typeof record.created === "string" ? { created: record.created } : {}),
52
+ images,
53
+ };
54
+ }
55
+ export async function listMoodboards(client) {
56
+ const raw = await client.request(ENDPOINTS.moodboards);
57
+ return asArray(raw)
58
+ .map(normaliseMoodboard)
59
+ .filter((board) => board !== undefined);
60
+ }
61
+ export async function listProfiles(client) {
62
+ const raw = await client.request(ENDPOINTS.personalizedProfiles);
63
+ return asArray(raw)
64
+ .map((record) => {
65
+ const id = record.id;
66
+ if (typeof id !== "string" && typeof id !== "number")
67
+ return undefined;
68
+ return {
69
+ id: String(id),
70
+ title: typeof record.title === "string" ? record.title : "(untitled)",
71
+ ...(typeof record.rankingCount === "number" ? { rankingCount: record.rankingCount } : {}),
72
+ ...(typeof record.majorVersion === "string" ? { majorVersion: record.majorVersion } : {}),
73
+ };
74
+ })
75
+ .filter((profile) => profile !== undefined);
76
+ }
77
+ /**
78
+ * Find a moodboard by the name someone would say, or by id.
79
+ *
80
+ * Exact title first, then case-insensitive, then a unique substring. An
81
+ * ambiguous substring is an error naming the candidates rather than a silent
82
+ * pick: choosing "Porsche Campaign" when someone meant "Mercedes Campaign"
83
+ * would be wrong in a way that costs a generation to discover.
84
+ */
85
+ export function matchMoodboard(boards, query) {
86
+ const wanted = query.trim();
87
+ if (!wanted)
88
+ throw new NotFoundError("A moodboard name or id is required.", 404, "(local)");
89
+ const byId = boards.find((board) => board.id === wanted);
90
+ if (byId)
91
+ return byId;
92
+ const exact = boards.find((board) => board.title === wanted);
93
+ if (exact)
94
+ return exact;
95
+ const lower = wanted.toLowerCase();
96
+ const insensitive = boards.filter((board) => board.title.toLowerCase() === lower);
97
+ if (insensitive.length === 1 && insensitive[0])
98
+ return insensitive[0];
99
+ const partial = boards.filter((board) => board.title.toLowerCase().includes(lower));
100
+ if (partial.length === 1 && partial[0])
101
+ return partial[0];
102
+ if (partial.length > 1) {
103
+ throw new NotFoundError(`'${query}' matches ${partial.length} moodboards: ${partial.map((board) => board.title).join(", ")}. Name one exactly.`, 404, "(local)");
104
+ }
105
+ throw new NotFoundError(`No moodboard called '${query}'. Available: ${boards.map((board) => board.title).join(", ") || "none"}.`, 404, "(local)");
106
+ }
107
+ /**
108
+ * Pick references out of a moodboard.
109
+ *
110
+ * Every image would make an unusable prompt: a 242-image board becomes 242
111
+ * `--sref` flags. A handful is what the web app effectively does too, and
112
+ * spreading the pick across the board rather than taking the first few avoids
113
+ * always drawing on whatever was added first.
114
+ */
115
+ export function pickReferences(board, count) {
116
+ const wanted = Math.max(1, Math.min(Math.trunc(count), 10));
117
+ const urls = board.images.map((image) => image.url);
118
+ if (urls.length === 0) {
119
+ throw new NotFoundError(`Moodboard '${board.title}' has no images, so there is nothing to reference. Add some in the web app first.`, 404, "(local)");
120
+ }
121
+ if (urls.length <= wanted)
122
+ return urls;
123
+ const step = urls.length / wanted;
124
+ return Array.from({ length: wanted }, (_, index) => urls[Math.floor(index * step)]);
125
+ }
126
+ /** Resolve "use my High Fashion moodboard" to the style references it means. */
127
+ export async function referencesFromMoodboard(client, query, count) {
128
+ const board = matchMoodboard(await listMoodboards(client), query);
129
+ return { board, refs: pickReferences(board, count) };
130
+ }
131
+ function imageRecord(image) {
132
+ return {
133
+ url: image.url,
134
+ width: image.width ?? null,
135
+ height: image.height ?? null,
136
+ created: Date.now(),
137
+ // The app uses these to render a tile before the server confirms it. We are
138
+ // not rendering anything, so the record is sent already settled.
139
+ optimistic: false,
140
+ optimisticId: null,
141
+ state: null,
142
+ };
143
+ }
144
+ export async function createMoodboard(client, title) {
145
+ const name = title.trim();
146
+ if (!name)
147
+ throw new NotFoundError("A moodboard needs a title.", 400, "(local)");
148
+ const userId = await client.userId();
149
+ const raw = await client.request(ENDPOINTS.moodboards, {
150
+ method: "POST",
151
+ body: { session_id: userId, title: name },
152
+ noRetry: true,
153
+ });
154
+ // The response shape is not documented, so the id is looked for rather than
155
+ // assumed, and its absence is not treated as a failure: the board exists.
156
+ let id;
157
+ if (raw && typeof raw === "object") {
158
+ const record = raw;
159
+ const candidate = record.moodboard_id ?? record.id;
160
+ if (typeof candidate === "string" || typeof candidate === "number")
161
+ id = String(candidate);
162
+ }
163
+ return { ...(id ? { id } : {}), title: name, raw };
164
+ }
165
+ export async function addToMoodboard(client, moodboardId, images) {
166
+ if (images.length === 0) {
167
+ throw new NotFoundError("Nothing to add: no image URLs were given.", 400, "(local)");
168
+ }
169
+ const userId = await client.userId();
170
+ const raw = await client.request(`${ENDPOINTS.moodboards}?moodboard_id=${encodeURIComponent(moodboardId)}`, {
171
+ method: "PATCH",
172
+ body: { session_id: userId, add: images.map(imageRecord) },
173
+ noRetry: true,
174
+ });
175
+ return { added: images.length, raw };
176
+ }
177
+ export async function removeFromMoodboard(client, moodboardId, urls) {
178
+ if (urls.length === 0) {
179
+ throw new NotFoundError("Nothing to remove: no image URLs were given.", 400, "(local)");
180
+ }
181
+ const userId = await client.userId();
182
+ const raw = await client.request(`${ENDPOINTS.moodboards}?moodboard_id=${encodeURIComponent(moodboardId)}`, {
183
+ method: "PATCH",
184
+ body: { session_id: userId, remove: urls.map((url) => ({ url })) },
185
+ noRetry: true,
186
+ });
187
+ return { removed: urls.length, raw };
188
+ }
189
+ //# sourceMappingURL=moodboards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moodboards.js","sourceRoot":"","sources":["../../src/api/moodboards.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Record what the Midjourney web app actually calls.
3
+ *
4
+ * Midjourney has no API documentation, so every endpoint this server knows
5
+ * about was learned by watching the site. The reference implementation for this
6
+ * API did that once, by hand, and froze the result: twelve read endpoints from
7
+ * a single browsing session, and no way for anyone to extend it without
8
+ * repeating the whole exercise.
9
+ *
10
+ * Making it a command changes that. Run capture, use the site normally, click
11
+ * the thing that has no tool yet, and the request appears in the output with
12
+ * its method, path, query and body. That is the input to a new tool, and it
13
+ * takes a minute rather than an afternoon.
14
+ *
15
+ * It records the account's own traffic in its own browser. It never touches the
16
+ * request headers, which is deliberate: the interesting part is the shape of
17
+ * the call, and writing session cookies into a file people will paste into
18
+ * issues is a good way to leak an account.
19
+ */
20
+ import type { Config } from "./config.js";
21
+ export type CaptureOptions = {
22
+ seconds: number;
23
+ outPath?: string;
24
+ /** Record every request, not only the ones under /api/. */
25
+ all?: boolean;
26
+ };
27
+ export declare function runCapture(config: Config, options: CaptureOptions): Promise<number>;