@mimeticinc/mim-cli 0.1.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.
@@ -0,0 +1,1181 @@
1
+ // src/mim-cli.ts
2
+ import { spawn } from "child_process";
3
+ import { randomUUID } from "crypto";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { pathToFileURL } from "url";
7
+ var DEFAULT_MIM_API_BASE_URL = "https://trymimetic.com";
8
+ var MIM_CLIENT_VERSION = "0.1.0";
9
+ function mimUsage() {
10
+ return `mim
11
+
12
+ TryMimetic growth context for Claude Code, Codex, and shell workflows.
13
+
14
+ Commands:
15
+ auth login Authenticate this machine with TryMimetic.
16
+ auth login --token <token> Store an existing API token.
17
+ auth status Show the active account and default project.
18
+ auth logout Remove local Mimetic credentials.
19
+ projects List projects/sites available to this account.
20
+ context Print a growth context pack for a project.
21
+ recordings List recent session recording summaries.
22
+ replay [session_id] Print one persisted replay insight. Defaults to latest.
23
+ findings List audit/backlog findings from TryMimetic.
24
+ audit start <url> Start an audit, reusing a running/completed audit when available.
25
+ audit rerun <url> Force a fresh audit for a site that was already audited.
26
+ audit status <audit_id> Check audit progress.
27
+ audit wait <audit_id> Poll until an audit completes or fails.
28
+ web-audit <url> Run web SDK auditor locally (extracts analytics keys, dumps configs).
29
+ ipa-audit <path> Run static IPA analyzer locally (SDKs, permissions, privacy).
30
+ fixes list List fixable findings and PR/review state.
31
+ fixes start <rank> Queue a code fix for a finding from fixes list.
32
+ fixes open <rank> Open the PR, review, preview, or workflow for a fix.
33
+ billing status Show Mimetic billing status for a project.
34
+ billing checkout [plan] Print a Stripe Checkout URL. Plan defaults to starter.
35
+ billing open [plan] Open Stripe Checkout in your browser.
36
+ billing portal Open the Stripe billing portal when available.
37
+ mcp Print MCP setup help.
38
+ mcp install claude Print a Claude Code MCP install command.
39
+ mcp install codex Print Codex MCP config.
40
+
41
+ Common options:
42
+ --project <key> Project/site key. Defaults to MIM_PROJECT or saved default.
43
+ --format <json|markdown> Output format. Default: markdown for context, json for lists.
44
+ --limit <n> Number of items to request, up to 100. Default: 20.
45
+ --api-base-url <url> TryMimetic API origin. Default: MIM_API_BASE_URL or ${DEFAULT_MIM_API_BASE_URL}.
46
+ --json Alias for --format json.
47
+ --mode <slim|full> Audit mode. Default: slim (~100 checks). full = all 57 checks + SDK intel.
48
+ --wait With audit start/rerun, poll until completion.
49
+ --target <kind> With fixes open, choose review, preview, pr, diff, workflow, or report.
50
+ --run With mcp install claude, run the install command instead of printing it.
51
+
52
+ Environment:
53
+ MIM_API_TOKEN Overrides the locally stored access token.
54
+ MIM_API_BASE_URL TryMimetic API origin.
55
+ MIM_PROJECT Default project/site key.
56
+ MIM_CONFIG_DIR Credential directory. Default: ~/.mim.
57
+ MIM_TELEMETRY=0 Disable metadata-only CLI/MCP usage events.
58
+ MIM_DISABLE_TELEMETRY=1 Disable metadata-only CLI/MCP usage events.
59
+ MIM_IOS_AUDIT_TOOL_DIR Path to ios-audit-tool directory for web-audit and ipa-audit.
60
+ `;
61
+ }
62
+ function readValue(args, index, flag) {
63
+ const value = args[index + 1];
64
+ if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
65
+ return [value, index + 1];
66
+ }
67
+ function homeDir(env = process.env) {
68
+ return env.HOME || env.USERPROFILE || "";
69
+ }
70
+ function mimConfigDir(env = process.env) {
71
+ const configured = env.MIM_CONFIG_DIR?.trim();
72
+ if (configured) return configured;
73
+ const home = homeDir(env);
74
+ if (!home) throw new Error("MIM_CONFIG_DIR or HOME is required to store Mimetic credentials");
75
+ return join(home, ".mim");
76
+ }
77
+ function mimConfigPath(env = process.env) {
78
+ return join(mimConfigDir(env), "config.json");
79
+ }
80
+ function loadMimConfig(env = process.env) {
81
+ let path = "";
82
+ try {
83
+ path = mimConfigPath(env);
84
+ } catch {
85
+ return {};
86
+ }
87
+ if (!existsSync(path)) return {};
88
+ try {
89
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
90
+ return parsed && typeof parsed === "object" ? parsed : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+ function saveMimConfig(config, env = process.env) {
96
+ const dir = mimConfigDir(env);
97
+ mkdirSync(dir, { recursive: true, mode: 448 });
98
+ const path = mimConfigPath(env);
99
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
100
+ `, { mode: 384 });
101
+ }
102
+ function clearMimConfig(env = process.env) {
103
+ const path = mimConfigPath(env);
104
+ if (existsSync(path)) rmSync(path);
105
+ }
106
+ function normalizeMimApiBaseUrl(value, source = "MIM_API_BASE_URL") {
107
+ const raw = (value || DEFAULT_MIM_API_BASE_URL).trim().replace(/\/+$/, "");
108
+ let parsed;
109
+ try {
110
+ parsed = new URL(raw);
111
+ } catch {
112
+ throw new Error(`${source} must be an absolute http(s) URL`);
113
+ }
114
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
115
+ throw new Error(`${source} must use http or https`);
116
+ }
117
+ return parsed.toString().replace(/\/$/, "");
118
+ }
119
+ function mimApiBaseUrl(config = {}, env = process.env) {
120
+ return normalizeMimApiBaseUrl(env.MIM_API_BASE_URL || config.apiBaseUrl || DEFAULT_MIM_API_BASE_URL);
121
+ }
122
+ function mimAuthToken(config = {}, env = process.env) {
123
+ return env.MIM_API_TOKEN?.trim() || config.accessToken?.trim() || "";
124
+ }
125
+ function defaultMimProject(config = {}, env = process.env) {
126
+ return env.MIM_PROJECT?.trim() || config.defaultProject?.trim() || "";
127
+ }
128
+ function runtimeFromArgs(args) {
129
+ let apiBaseUrlFlag = "";
130
+ let project = "";
131
+ const kept = [];
132
+ const config = loadMimConfig();
133
+ for (let i = 0; i < args.length; i += 1) {
134
+ const arg = args[i];
135
+ if (arg === "--api-base-url") {
136
+ const [value, next] = readValue(args, i, arg);
137
+ apiBaseUrlFlag = value;
138
+ i = next;
139
+ } else if (arg === "--project") {
140
+ const [value, next] = readValue(args, i, arg);
141
+ project = value;
142
+ i = next;
143
+ } else {
144
+ kept.push(arg);
145
+ }
146
+ }
147
+ const apiBaseUrl = normalizeMimApiBaseUrl(apiBaseUrlFlag || process.env.MIM_API_BASE_URL || config.apiBaseUrl || DEFAULT_MIM_API_BASE_URL);
148
+ const token = mimAuthToken(config);
149
+ const resolvedProject = project || defaultMimProject(config);
150
+ return {
151
+ runtime: {
152
+ apiBaseUrl,
153
+ token,
154
+ config,
155
+ traceId: randomUUID()
156
+ },
157
+ args: kept,
158
+ project: resolvedProject
159
+ };
160
+ }
161
+ function requireAuth(runtime) {
162
+ if (!runtime.token) {
163
+ throw new Error("not authenticated; run `mim auth login` or set MIM_API_TOKEN");
164
+ }
165
+ }
166
+ function requireProject(project) {
167
+ if (!project) throw new Error("--project is required; run `mim projects` or set MIM_PROJECT");
168
+ return project;
169
+ }
170
+ function projectPath(path, project) {
171
+ return path.replace(":project", encodeURIComponent(project));
172
+ }
173
+ async function mimRequest(runtime, path, options = {}, fetchImpl = fetch) {
174
+ const response = await fetchImpl(`${runtime.apiBaseUrl}${path}`, {
175
+ method: options.method || (options.body == null ? "GET" : "POST"),
176
+ headers: {
177
+ accept: options.acceptText ? "text/plain, application/json" : "application/json",
178
+ ...options.body == null ? {} : { "content-type": "application/json" },
179
+ ...runtime.token ? { authorization: `Bearer ${runtime.token}` } : {},
180
+ "x-mim-client": "mim-cli",
181
+ "x-mim-client-version": MIM_CLIENT_VERSION,
182
+ "x-mim-trace-id": runtime.traceId,
183
+ ...options.headers || {}
184
+ },
185
+ body: options.body == null ? void 0 : JSON.stringify(options.body)
186
+ });
187
+ if (!response.ok) {
188
+ const detail = (await response.text()).slice(0, 500);
189
+ throw new Error(`${path} failed with ${response.status}${detail ? `: ${detail}` : ""}`);
190
+ }
191
+ if (options.acceptText) return await response.text();
192
+ return await response.json();
193
+ }
194
+ function shouldSendMimTelemetry(env = process.env) {
195
+ const disabled = env.MIM_DISABLE_TELEMETRY?.trim().toLowerCase();
196
+ if (disabled && !["0", "false", "no", "off"].includes(disabled)) return false;
197
+ const value = env.MIM_TELEMETRY?.trim().toLowerCase();
198
+ return value == null || !["0", "false", "no", "off"].includes(value);
199
+ }
200
+ function sanitizeMimMetadata(value, depth = 0) {
201
+ if (depth > 4) return "[truncated]";
202
+ if (value == null || typeof value === "boolean" || typeof value === "number") return value;
203
+ if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
204
+ if (Array.isArray(value)) return value.slice(0, 20).map((item) => sanitizeMimMetadata(item, depth + 1));
205
+ if (typeof value === "object") {
206
+ const result = {};
207
+ for (const [key, item] of Object.entries(value).slice(0, 40)) {
208
+ const normalized = key.toLowerCase();
209
+ if (/(token|secret|authorization|password|credential|cookie|session|narrative|prompt|message|content|raw|body)/.test(normalized)) {
210
+ result[key] = "[redacted]";
211
+ } else {
212
+ result[key] = sanitizeMimMetadata(item, depth + 1);
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ return String(value);
218
+ }
219
+ function buildMimUsageEvent(input) {
220
+ return {
221
+ ...input,
222
+ client_version: MIM_CLIENT_VERSION,
223
+ metadata: input.metadata ? sanitizeMimMetadata(input.metadata) : void 0
224
+ };
225
+ }
226
+ async function trackMimUsageEvent(runtime, event, fetchImpl = fetch, env = process.env) {
227
+ if (!shouldSendMimTelemetry(env)) return;
228
+ const payload = buildMimUsageEvent({
229
+ ...event,
230
+ trace_id: event.trace_id || runtime.traceId
231
+ });
232
+ try {
233
+ await fetchImpl(`${runtime.apiBaseUrl}/api/mim/usage-events`, {
234
+ method: "POST",
235
+ headers: {
236
+ "content-type": "application/json",
237
+ ...runtime.token ? { authorization: `Bearer ${runtime.token}` } : {}
238
+ },
239
+ body: JSON.stringify(payload)
240
+ });
241
+ } catch {
242
+ }
243
+ }
244
+ function errorTelemetryMetadata(error) {
245
+ const name = error instanceof Error && error.name ? error.name : "Error";
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ const statusMatch = message.match(/\bfailed with (\d{3})\b/);
248
+ return {
249
+ error_name: name,
250
+ error_status: statusMatch ? Number(statusMatch[1]) : void 0,
251
+ error_message: message.slice(0, 240)
252
+ };
253
+ }
254
+ async function runTrackedMimCommand(runtime, details, fn) {
255
+ const started = Date.now();
256
+ try {
257
+ const result = await fn();
258
+ await trackMimUsageEvent(runtime, {
259
+ event: "cli_command",
260
+ client: "mim-cli",
261
+ command: details.command,
262
+ project: details.project,
263
+ status: "success",
264
+ duration_ms: Date.now() - started,
265
+ metadata: details.metadata
266
+ });
267
+ return result;
268
+ } catch (error) {
269
+ await trackMimUsageEvent(runtime, {
270
+ event: "cli_command",
271
+ client: "mim-cli",
272
+ command: details.command,
273
+ project: details.project,
274
+ status: "failure",
275
+ duration_ms: Date.now() - started,
276
+ metadata: {
277
+ ...details.metadata || {},
278
+ ...errorTelemetryMetadata(error)
279
+ }
280
+ });
281
+ throw error;
282
+ }
283
+ }
284
+ function parseFormat(args, defaultFormat) {
285
+ let format = defaultFormat;
286
+ const kept = [];
287
+ for (let i = 0; i < args.length; i += 1) {
288
+ const arg = args[i];
289
+ if (arg === "--json") {
290
+ format = "json";
291
+ } else if (arg === "--format") {
292
+ const [value, next] = readValue(args, i, arg);
293
+ if (value !== "json" && value !== "markdown") throw new Error("--format must be json or markdown");
294
+ format = value;
295
+ i = next;
296
+ } else {
297
+ kept.push(arg);
298
+ }
299
+ }
300
+ return { format, args: kept };
301
+ }
302
+ var MIM_LIST_LIMIT_MAX = 100;
303
+ function parseLimit(args, defaultLimit = 20, maxLimit = MIM_LIST_LIMIT_MAX) {
304
+ let limit = defaultLimit;
305
+ const kept = [];
306
+ for (let i = 0; i < args.length; i += 1) {
307
+ const arg = args[i];
308
+ if (arg === "--limit") {
309
+ const [value, next] = readValue(args, i, arg);
310
+ limit = Number(value);
311
+ i = next;
312
+ } else {
313
+ kept.push(arg);
314
+ }
315
+ }
316
+ if (!Number.isInteger(limit) || limit <= 0 || limit > maxLimit) throw new Error(`--limit must be an integer between 1 and ${maxLimit}`);
317
+ return { limit, args: kept };
318
+ }
319
+ function printJson(value) {
320
+ console.log(JSON.stringify(value, null, 2));
321
+ }
322
+ function sleep(ms) {
323
+ return new Promise((resolve) => setTimeout(resolve, ms));
324
+ }
325
+ function formatMimListPayload(payload, key) {
326
+ const items = Array.isArray(payload) ? payload : Array.isArray(payload?.[key]) ? payload[key] : [];
327
+ if (items.length === 0) return `No ${key.replace(/_/g, " ")} found.`;
328
+ return items.map((item, index) => {
329
+ if (!item || typeof item !== "object") return `${index + 1}. ${String(item)}`;
330
+ const obj = item;
331
+ const title = obj.name || obj.title || obj.sessionId || obj.id || obj.audit_id || obj.project || `item ${index + 1}`;
332
+ const status = obj.status ? ` status=${obj.status}` : "";
333
+ const url = obj.url || obj.startUrl || obj.start_url || "";
334
+ return `${index + 1}. ${title}${status}${url ? ` ${url}` : ""}`;
335
+ }).join("\n");
336
+ }
337
+ function auditId(payload) {
338
+ return payload.audit_id || payload.auditId || "";
339
+ }
340
+ function auditReportUrl(payload) {
341
+ return payload.report_url || payload.reportUrl || "";
342
+ }
343
+ function auditStatusUrl(payload) {
344
+ return payload.status_url || payload.statusUrl || "";
345
+ }
346
+ function formatAuditPayload(payload) {
347
+ const id = auditId(payload);
348
+ const lines = [
349
+ `Audit ${id || "(unknown)"} status=${payload.status || "unknown"}${payload.progress == null ? "" : ` progress=${payload.progress}%`}`
350
+ ];
351
+ if (payload.reused) lines.push("Reused an existing audit. Use `mim audit rerun <url>` to force a fresh audit.");
352
+ if (payload.message) lines.push(String(payload.message));
353
+ if (payload.project_key || payload.projectKey) lines.push(`Project: ${payload.project_key || payload.projectKey}`);
354
+ if (auditReportUrl(payload)) lines.push(`Report: ${auditReportUrl(payload)}`);
355
+ if (auditStatusUrl(payload)) lines.push(`Status API: ${auditStatusUrl(payload)}`);
356
+ return lines.join("\n");
357
+ }
358
+ function asRecord(value) {
359
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
360
+ }
361
+ function decodeSlackEscapes(value) {
362
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/gi, "'");
363
+ }
364
+ function displayText(value) {
365
+ return decodeSlackEscapes(String(value));
366
+ }
367
+ function firstDefined(...values) {
368
+ return values.find((value) => value !== void 0 && value !== null);
369
+ }
370
+ function stringList(value) {
371
+ return Array.isArray(value) ? value.map((item) => displayText(item)).filter(Boolean) : [];
372
+ }
373
+ function formatRecordingsPayload(payload, options = {}) {
374
+ const items = Array.isArray(payload) ? payload : Array.isArray(payload?.recordings) ? payload.recordings : [];
375
+ if (!items.length) {
376
+ const hint = String(payload?.hint || "").trim();
377
+ return hint || "No replay insights found yet.";
378
+ }
379
+ const lines = ["# Recent Recordings"];
380
+ for (const item of items) {
381
+ const recording = asRecord(item);
382
+ const summary = asRecord(recording.summary);
383
+ const id = String(recording.session_id || recording.sessionId || recording.id || "");
384
+ const priority = String(recording.priority || "low").toUpperCase();
385
+ const url = String(recording.url || recording.start_url || recording.startUrl || summary.start_url || "");
386
+ const duration = firstDefined(recording.duration_s, recording.durationS, summary.duration_s);
387
+ const events = firstDefined(recording.event_count, recording.eventCount, summary.event_count);
388
+ const clicks = firstDefined(recording.click_count, recording.clickCount, summary.click_count);
389
+ const inputs = firstDefined(recording.input_count, recording.inputCount, summary.input_count);
390
+ const errors = firstDefined(recording.console_error_count, recording.consoleErrorCount, summary.console_error_count);
391
+ const device = String(recording.device || summary.device || "");
392
+ const userAgent = String(recording.user_agent || recording.userAgent || summary.user_agent || "");
393
+ const source = String(recording.interpretation_source || recording.interpretationSource || "");
394
+ lines.push("", `## ${id || "session"} [${priority}]`);
395
+ if (url) lines.push(`URL: ${url}`);
396
+ const counts = [
397
+ duration == null ? "" : `Duration: ${duration}s`,
398
+ events == null ? "" : `Events: ${events}`,
399
+ clicks == null ? "" : `Clicks: ${clicks}`,
400
+ inputs == null ? "" : `Inputs: ${inputs}`,
401
+ errors == null ? "" : `Console errors: ${errors}`
402
+ ].filter(Boolean);
403
+ if (counts.length) lines.push(counts.join(" | "));
404
+ if (device) lines.push(`Device: ${device}`);
405
+ if (userAgent) lines.push(`User agent: ${userAgent}`);
406
+ if (source) lines.push(`Insight source: ${source}`);
407
+ const whatHappened = stringList(recording.what_happened || recording.whatHappened);
408
+ if (whatHappened.length) {
409
+ lines.push("What happened:");
410
+ for (const detail of whatHappened) lines.push(`- ${detail}`);
411
+ }
412
+ const likelyFriction = stringList(recording.likely_friction || recording.likelyFriction);
413
+ if (likelyFriction.length) {
414
+ lines.push("Likely friction:");
415
+ for (const detail of likelyFriction) lines.push(`- ${detail}`);
416
+ }
417
+ if (!whatHappened.length && !likelyFriction.length && id) {
418
+ lines.push(`Insight details: run mim replay ${id}.`);
419
+ }
420
+ if (id) {
421
+ const projectArg = options.project ? ` --project ${options.project}` : "";
422
+ lines.push(`Replay: mim replay ${id}${projectArg}`);
423
+ }
424
+ }
425
+ return lines.join("\n");
426
+ }
427
+ function replayInsightFromPayload(payload) {
428
+ const root = asRecord(payload);
429
+ const insight = asRecord(root.insight);
430
+ if (Object.keys(insight).length) return insight;
431
+ const recording = asRecord(root.recording);
432
+ if (Object.keys(recording).length) return recording;
433
+ return root;
434
+ }
435
+ function formatReplayInsightPayload(payload) {
436
+ const insight = replayInsightFromPayload(payload);
437
+ const summary = asRecord(insight.summary);
438
+ const sessionId = String(insight.session_id || insight.sessionId || insight.id || "");
439
+ const title = sessionId ? `Replay ${sessionId}` : "Replay insight";
440
+ const lines = [`# ${title}`];
441
+ const priority = insight.priority ? String(insight.priority).toUpperCase() : "";
442
+ const url = String(insight.url || insight.start_url || summary.start_url || "");
443
+ const device = String(insight.device || summary.device || "");
444
+ const userAgent = String(insight.user_agent || insight.userAgent || summary.user_agent || "");
445
+ const source = String(insight.interpretation_source || insight.interpretationSource || "");
446
+ const touchedAt = insight.touched_at_ms || insight.touchedAtMs || insight.touched_at || "";
447
+ if (priority) lines.push(`Priority: ${priority}`);
448
+ if (url) lines.push(`URL: ${url}`);
449
+ if (device) lines.push(`Device: ${device}`);
450
+ if (userAgent) lines.push(`User agent: ${userAgent}`);
451
+ if (source) lines.push(`Insight source: ${source}`);
452
+ if (touchedAt) lines.push(`Touched at: ${touchedAt}`);
453
+ const whatHappened = stringList(insight.what_happened || insight.whatHappened);
454
+ if (whatHappened.length) {
455
+ lines.push("", "## What Happened", ...whatHappened.map((item) => `- ${item}`));
456
+ }
457
+ const likelyFriction = stringList(insight.likely_friction || insight.likelyFriction);
458
+ if (likelyFriction.length) {
459
+ lines.push("", "## Likely Friction", ...likelyFriction.map((item) => `- ${item}`));
460
+ }
461
+ const interpretation = asRecord(insight.interpretation);
462
+ const issues = Array.isArray(interpretation.issues) ? interpretation.issues : [];
463
+ if (issues.length) {
464
+ lines.push("", "## Interpreted Issues");
465
+ for (const issue of issues.slice(0, 8)) {
466
+ const item = asRecord(issue);
467
+ const severity = item.severity ? `[${String(item.severity).toUpperCase()}] ` : "";
468
+ const issueTitle = displayText(item.title || "Issue");
469
+ const wrong = item.what_went_wrong ? `: ${displayText(item.what_went_wrong)}` : "";
470
+ const cause = item.likely_cause ? ` Likely cause: ${displayText(item.likely_cause)}` : "";
471
+ lines.push(`- ${severity}${issueTitle}${wrong}${cause}`);
472
+ }
473
+ }
474
+ const narrative = String(insight.narrative || "").trim();
475
+ if (narrative) {
476
+ lines.push("", "## Redacted Narrative", "```csv", narrative, "```");
477
+ }
478
+ return lines.join("\n");
479
+ }
480
+ function openBrowser(url) {
481
+ const platform = process.platform;
482
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
483
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
484
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
485
+ child.unref();
486
+ }
487
+ async function pollDeviceToken(runtime, deviceCode, intervalSec, expiresInSec) {
488
+ const deadline = Date.now() + expiresInSec * 1e3;
489
+ while (Date.now() < deadline) {
490
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1, intervalSec) * 1e3));
491
+ try {
492
+ return await mimRequest(runtime, "/api/mim/cli/token", {
493
+ method: "POST",
494
+ body: { device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }
495
+ });
496
+ } catch (error) {
497
+ const message = error instanceof Error ? error.message : "";
498
+ if (message.includes("428") || message.includes("authorization_pending") || message.includes("slow_down")) continue;
499
+ throw error;
500
+ }
501
+ }
502
+ throw new Error("login timed out; run `mim auth login` again");
503
+ }
504
+ async function runAuth(args) {
505
+ const [subcommand = "status", ...rest] = args;
506
+ const { runtime, args: kept } = runtimeFromArgs(rest);
507
+ if (subcommand === "logout") {
508
+ clearMimConfig();
509
+ console.log("Logged out of Mimetic.");
510
+ return;
511
+ }
512
+ if (subcommand === "status") {
513
+ const token2 = runtime.token;
514
+ if (!token2) {
515
+ console.log("Not authenticated. Run `mim auth login`.");
516
+ return;
517
+ }
518
+ const email = runtime.config.user?.email || "(token configured)";
519
+ const project = defaultMimProject(runtime.config) || "(none)";
520
+ console.log(`Authenticated as ${email}`);
521
+ console.log(`API: ${runtime.apiBaseUrl}`);
522
+ console.log(`Default project: ${project}`);
523
+ return;
524
+ }
525
+ if (subcommand !== "login") throw new Error(`unknown auth command: ${subcommand}`);
526
+ let token = "";
527
+ let noOpen = false;
528
+ for (let i = 0; i < kept.length; i += 1) {
529
+ const arg = kept[i];
530
+ if (arg === "--token") {
531
+ const [value, next] = readValue(kept, i, arg);
532
+ token = value;
533
+ i = next;
534
+ } else if (arg === "--no-open") {
535
+ noOpen = true;
536
+ } else {
537
+ throw new Error(`unknown auth login option: ${arg}`);
538
+ }
539
+ }
540
+ if (token) {
541
+ saveMimConfig({ ...runtime.config, apiBaseUrl: runtime.apiBaseUrl, accessToken: token });
542
+ console.log("Mimetic API token saved.");
543
+ return;
544
+ }
545
+ const started = await mimRequest(runtime, "/api/mim/cli/device", {
546
+ method: "POST",
547
+ body: { client: "mim-cli", version: MIM_CLIENT_VERSION }
548
+ });
549
+ const deviceCode = started.device_code || started.deviceCode || "";
550
+ const userCode = started.user_code || started.userCode || "";
551
+ const verificationUri = started.verification_uri_complete || started.verificationUriComplete || started.verification_uri || started.verificationUri || "";
552
+ if (!deviceCode || !verificationUri) throw new Error("TryMimetic did not return a device login URL");
553
+ console.log("Open this URL to finish Mimetic login:");
554
+ console.log(` ${verificationUri}`);
555
+ if (userCode) console.log(`Code: ${userCode}`);
556
+ if (!noOpen) openBrowser(verificationUri);
557
+ const tokenResponse = await pollDeviceToken(runtime, deviceCode, started.interval || 3, started.expires_in || started.expiresIn || 600);
558
+ const accessToken = tokenResponse.access_token || tokenResponse.accessToken || "";
559
+ if (!accessToken) throw new Error("TryMimetic did not return an access token");
560
+ const nextConfig = {
561
+ ...runtime.config,
562
+ apiBaseUrl: runtime.apiBaseUrl,
563
+ accessToken,
564
+ refreshToken: tokenResponse.refresh_token || tokenResponse.refreshToken,
565
+ expiresAt: tokenResponse.expires_at || tokenResponse.expiresAt,
566
+ defaultProject: tokenResponse.default_project || tokenResponse.defaultProject || runtime.config.defaultProject,
567
+ user: tokenResponse.user || runtime.config.user
568
+ };
569
+ saveMimConfig(nextConfig);
570
+ console.log(`Logged in${nextConfig.user?.email ? ` as ${nextConfig.user.email}` : ""}.`);
571
+ if (nextConfig.defaultProject) console.log(`Default project: ${nextConfig.defaultProject}`);
572
+ }
573
+ async function runProjects(args) {
574
+ const { runtime, args: kept } = runtimeFromArgs(args);
575
+ requireAuth(runtime);
576
+ const { format, args: afterFormat } = parseFormat(kept, "json");
577
+ if (afterFormat.length) throw new Error(`unknown projects option: ${afterFormat[0]}`);
578
+ const payload = await runTrackedMimCommand(
579
+ runtime,
580
+ { command: "projects", metadata: { format } },
581
+ () => mimRequest(runtime, "/api/mim/projects")
582
+ );
583
+ if (format === "json") printJson(payload);
584
+ else console.log(formatMimListPayload(payload, "projects"));
585
+ }
586
+ async function runContext(args) {
587
+ const { runtime, args: kept, project } = runtimeFromArgs(args);
588
+ requireAuth(runtime);
589
+ const resolvedProject = requireProject(project);
590
+ const parsedFormat = parseFormat(kept, "markdown");
591
+ const parsedLimit = parseLimit(parsedFormat.args, 20);
592
+ if (parsedLimit.args.length) throw new Error(`unknown context option: ${parsedLimit.args[0]}`);
593
+ const path = projectPath(`/api/mim/projects/:project/context?format=${parsedFormat.format}&limit=${parsedLimit.limit}`, resolvedProject);
594
+ const payload = await runTrackedMimCommand(
595
+ runtime,
596
+ { command: "context", project: resolvedProject, metadata: { format: parsedFormat.format, limit: parsedLimit.limit } },
597
+ () => mimRequest(runtime, path, { acceptText: parsedFormat.format === "markdown" })
598
+ );
599
+ if (parsedFormat.format === "json") printJson(payload);
600
+ else console.log(String(payload).trim());
601
+ }
602
+ async function runCollectionCommand(command, args) {
603
+ const { runtime, args: kept, project } = runtimeFromArgs(args);
604
+ requireAuth(runtime);
605
+ const resolvedProject = requireProject(project);
606
+ const parsedFormat = parseFormat(kept, command === "recordings" ? "markdown" : "json");
607
+ const parsedLimit = parseLimit(parsedFormat.args, 20);
608
+ if (parsedLimit.args.length) throw new Error(`unknown ${command} option: ${parsedLimit.args[0]}`);
609
+ const path = projectPath(`/api/mim/projects/:project/${command}?limit=${parsedLimit.limit}`, resolvedProject);
610
+ const payload = await runTrackedMimCommand(
611
+ runtime,
612
+ { command, project: resolvedProject, metadata: { format: parsedFormat.format, limit: parsedLimit.limit } },
613
+ () => mimRequest(runtime, path)
614
+ );
615
+ if (parsedFormat.format === "json") printJson(payload);
616
+ else if (command === "recordings") console.log(formatRecordingsPayload(payload, { project: resolvedProject }));
617
+ else console.log(formatMimListPayload(payload, command));
618
+ }
619
+ async function runReplay(args) {
620
+ const { runtime, args: kept, project } = runtimeFromArgs(args);
621
+ requireAuth(runtime);
622
+ const resolvedProject = requireProject(project);
623
+ const parsedFormat = parseFormat(kept, "markdown");
624
+ let sessionId = "";
625
+ let latest = false;
626
+ for (let i = 0; i < parsedFormat.args.length; i += 1) {
627
+ const arg = parsedFormat.args[i];
628
+ if (arg === "--latest") {
629
+ latest = true;
630
+ } else if (arg === "--session" || arg === "--session-id") {
631
+ const [value, next] = readValue(parsedFormat.args, i, arg);
632
+ sessionId = value;
633
+ i = next;
634
+ } else if (!sessionId) {
635
+ sessionId = arg;
636
+ } else {
637
+ throw new Error(`unknown replay option: ${arg}`);
638
+ }
639
+ }
640
+ if (!sessionId || sessionId === "latest") {
641
+ sessionId = "latest";
642
+ latest = true;
643
+ }
644
+ const path = projectPath(`/api/mim/projects/:project/recordings/${encodeURIComponent(sessionId)}`, resolvedProject);
645
+ const payload = await runTrackedMimCommand(
646
+ runtime,
647
+ { command: "replay", project: resolvedProject, metadata: { format: parsedFormat.format, session_id: latest ? "latest" : sessionId } },
648
+ () => mimRequest(runtime, path)
649
+ );
650
+ if (parsedFormat.format === "json") printJson(payload);
651
+ else console.log(formatReplayInsightPayload(payload));
652
+ }
653
+ async function pollAuditCompletion(runtime, id, initial, format, pollIntervalSec) {
654
+ let payload = initial || await mimRequest(runtime, `/api/mim/audits/${encodeURIComponent(id)}/status`);
655
+ while (payload.status === "running" || payload.status === "pending") {
656
+ await sleep(pollIntervalSec * 1e3);
657
+ payload = await mimRequest(runtime, `/api/mim/audits/${encodeURIComponent(id)}/status`);
658
+ if (format !== "json") {
659
+ const progress = payload.progress == null ? "" : ` ${payload.progress}%`;
660
+ process.stderr.write(`audit ${id}: ${payload.status || "unknown"}${progress}
661
+ `);
662
+ }
663
+ }
664
+ return payload;
665
+ }
666
+ async function runAudit(args) {
667
+ const [subcommand, ...rest] = args;
668
+ if (!subcommand) throw new Error("audit command must be start, rerun, status, or wait");
669
+ const { runtime, args: kept } = runtimeFromArgs(rest);
670
+ requireAuth(runtime);
671
+ const parsedFormat = parseFormat(kept, "markdown");
672
+ if (subcommand === "status" || subcommand === "wait") {
673
+ let id = "";
674
+ let pollIntervalSec2 = 5;
675
+ for (let i = 0; i < parsedFormat.args.length; i += 1) {
676
+ const arg = parsedFormat.args[i];
677
+ if (arg === "--poll-interval") {
678
+ const [value, next] = readValue(parsedFormat.args, i, arg);
679
+ pollIntervalSec2 = Number(value);
680
+ i = next;
681
+ } else if (!id) {
682
+ id = arg;
683
+ } else {
684
+ throw new Error(`unknown audit ${subcommand} option: ${arg}`);
685
+ }
686
+ }
687
+ if (!id) throw new Error(`audit ${subcommand} requires an audit_id`);
688
+ if (!Number.isFinite(pollIntervalSec2) || pollIntervalSec2 < 1 || pollIntervalSec2 > 60) {
689
+ throw new Error("--poll-interval must be a number between 1 and 60");
690
+ }
691
+ const payload2 = await runTrackedMimCommand(
692
+ runtime,
693
+ { command: `audit ${subcommand}`, metadata: { audit_id: id, format: parsedFormat.format, wait: subcommand === "wait" } },
694
+ async () => {
695
+ const current = await mimRequest(runtime, `/api/mim/audits/${encodeURIComponent(id)}/status`);
696
+ return subcommand === "wait" ? pollAuditCompletion(runtime, id, current, parsedFormat.format, pollIntervalSec2) : current;
697
+ }
698
+ );
699
+ if (parsedFormat.format === "json") printJson(payload2);
700
+ else console.log(formatAuditPayload(payload2));
701
+ return;
702
+ }
703
+ if (subcommand !== "start" && subcommand !== "rerun") throw new Error(`unknown audit command: ${subcommand}`);
704
+ let url = "";
705
+ let email = "";
706
+ let mode = "slim";
707
+ let force = subcommand === "rerun";
708
+ let wait = false;
709
+ let pollIntervalSec = 5;
710
+ for (let i = 0; i < parsedFormat.args.length; i += 1) {
711
+ const arg = parsedFormat.args[i];
712
+ if (arg === "--force" || arg === "--reaudit") {
713
+ force = true;
714
+ } else if (arg === "--wait") {
715
+ wait = true;
716
+ } else if (arg === "--email") {
717
+ const [value, next] = readValue(parsedFormat.args, i, arg);
718
+ email = value;
719
+ i = next;
720
+ } else if (arg === "--mode") {
721
+ const [value, next] = readValue(parsedFormat.args, i, arg);
722
+ if (value !== "slim" && value !== "full") throw new Error("--mode must be slim or full");
723
+ mode = value;
724
+ i = next;
725
+ } else if (arg === "--poll-interval") {
726
+ const [value, next] = readValue(parsedFormat.args, i, arg);
727
+ pollIntervalSec = Number(value);
728
+ i = next;
729
+ } else if (!url) {
730
+ url = arg;
731
+ } else {
732
+ throw new Error(`unknown audit ${subcommand} option: ${arg}`);
733
+ }
734
+ }
735
+ if (!url) throw new Error(`audit ${subcommand} requires a URL`);
736
+ if (!Number.isFinite(pollIntervalSec) || pollIntervalSec < 1 || pollIntervalSec > 60) {
737
+ throw new Error("--poll-interval must be a number between 1 and 60");
738
+ }
739
+ const payload = await runTrackedMimCommand(
740
+ runtime,
741
+ { command: subcommand === "rerun" ? "audit rerun" : "audit start", metadata: { force, wait, mode, format: parsedFormat.format } },
742
+ async () => {
743
+ const startedPayload = await mimRequest(runtime, "/api/mim/audits", {
744
+ method: "POST",
745
+ body: { url, force, mode, ...email ? { email } : {} }
746
+ });
747
+ if (!wait) return startedPayload;
748
+ const id = auditId(startedPayload);
749
+ if (!id) throw new Error("TryMimetic did not return an audit_id");
750
+ return pollAuditCompletion(runtime, id, startedPayload, parsedFormat.format, pollIntervalSec);
751
+ }
752
+ );
753
+ if (parsedFormat.format === "json") printJson(payload);
754
+ else console.log(formatAuditPayload(payload));
755
+ }
756
+ function fixesFromPayload(payload) {
757
+ return Array.isArray(payload.fixes) ? payload.fixes.filter((item) => item && typeof item === "object") : [];
758
+ }
759
+ function fixState(item) {
760
+ return item.fix_state && typeof item.fix_state === "object" ? item.fix_state : {};
761
+ }
762
+ function fixUrl(item, target, payload) {
763
+ const state = fixState(item);
764
+ const byTarget = {
765
+ review: String(item.review_url || state.review_url || ""),
766
+ preview: String(item.preview_url || state.preview_url || ""),
767
+ pr: String(item.pr_url || state.pr_url || ""),
768
+ diff: String(item.diff_url || state.diff_url || ""),
769
+ workflow: String(item.workflow_run_url || state.workflow_run_url || ""),
770
+ report: String(payload?.report_url || "")
771
+ };
772
+ if (target) return byTarget[target] || "";
773
+ return byTarget.review || byTarget.preview || byTarget.pr || byTarget.diff || byTarget.workflow || byTarget.report || "";
774
+ }
775
+ function resolveFixItem(payload, identifier) {
776
+ const fixes = fixesFromPayload(payload);
777
+ const trimmed = identifier.trim();
778
+ const numeric = Number(trimmed);
779
+ let item;
780
+ if (Number.isInteger(numeric)) {
781
+ item = fixes.find((candidate) => candidate.rank === numeric) || fixes.find((candidate) => candidate.finding_index === numeric - 1) || fixes.find((candidate) => candidate.finding_index === numeric);
782
+ }
783
+ item || (item = fixes.find((candidate) => candidate.id === trimmed));
784
+ if (!item) throw new Error(`fix ${identifier} was not found; run \`mim fixes list\``);
785
+ return item;
786
+ }
787
+ function formatFixesPayload(payload) {
788
+ const project = payload.project || {};
789
+ const name = project.name || project.key || "project";
790
+ const lines = [`# Fixes: ${name}`];
791
+ if (project.url) lines.push(`Site: ${project.url}`);
792
+ if (payload.report_url) lines.push(`Report: ${payload.report_url}`);
793
+ if (!payload.can_start_fix) {
794
+ const missing = [
795
+ payload.fixes_enabled ? "" : "fixes disabled",
796
+ payload.fix_runner_configured ? "" : "fix runner not configured",
797
+ payload.github_connected ? "" : "GitHub not connected"
798
+ ].filter(Boolean);
799
+ if (missing.length) lines.push(`Start fix unavailable: ${missing.join(", ")}`);
800
+ }
801
+ const fixes = fixesFromPayload(payload);
802
+ if (!fixes.length) {
803
+ lines.push("", "No fixes found.");
804
+ return lines.join("\n");
805
+ }
806
+ for (const item of fixes) {
807
+ const state = fixState(item);
808
+ const rank = item.rank || (typeof item.finding_index === "number" ? item.finding_index + 1 : fixes.indexOf(item) + 1);
809
+ const severity = String(item.severity || "medium").toUpperCase();
810
+ const label = state.label || state.status || "Not requested";
811
+ lines.push("", `${rank}. [${severity}] ${item.title || `Finding ${rank}`}`);
812
+ lines.push(` State: ${label}`);
813
+ if (item.status) lines.push(` Finding status: ${item.status}`);
814
+ const review = fixUrl(item, "review", payload);
815
+ const preview = fixUrl(item, "preview", payload);
816
+ const pr = fixUrl(item, "pr", payload);
817
+ const diff = fixUrl(item, "diff", payload);
818
+ const workflow = fixUrl(item, "workflow", payload);
819
+ if (review) lines.push(` Review: ${review}`);
820
+ if (preview) lines.push(` Preview: ${preview}`);
821
+ if (pr) lines.push(` PR: ${pr}`);
822
+ if (diff) lines.push(` Diff: ${diff}`);
823
+ if (workflow) lines.push(` Workflow: ${workflow}`);
824
+ }
825
+ return lines.join("\n");
826
+ }
827
+ function formatFixStartPayload(payload) {
828
+ const root = asRecord(payload);
829
+ const lines = [String(root.message || "Fix request submitted.")];
830
+ const issue = asRecord(root.issue);
831
+ if (issue.title) lines.push(`Issue: ${issue.title}`);
832
+ if (issue.rank) lines.push(`Rank: ${issue.rank}`);
833
+ if (root.workflow_url) lines.push(`Workflow: ${root.workflow_url}`);
834
+ return lines.join("\n");
835
+ }
836
+ function parseFixTarget(args) {
837
+ let target = "";
838
+ const kept = [];
839
+ const valid = /* @__PURE__ */ new Set(["review", "preview", "pr", "diff", "workflow", "report"]);
840
+ for (let i = 0; i < args.length; i += 1) {
841
+ const arg = args[i];
842
+ if (arg === "--target") {
843
+ const [value, next] = readValue(args, i, arg);
844
+ if (!valid.has(value)) throw new Error("--target must be review, preview, pr, diff, workflow, or report");
845
+ target = value;
846
+ i = next;
847
+ } else if (arg === "--review" || arg === "--preview" || arg === "--pr" || arg === "--diff" || arg === "--workflow" || arg === "--report") {
848
+ target = arg.slice(2);
849
+ } else {
850
+ kept.push(arg);
851
+ }
852
+ }
853
+ return { target, args: kept };
854
+ }
855
+ async function fetchFixes(runtime, project, limit) {
856
+ const path = projectPath(`/api/mim/projects/:project/fixes?limit=${limit}`, project);
857
+ return mimRequest(runtime, path);
858
+ }
859
+ async function runFixes(args) {
860
+ const [subcommand = "list", ...rest] = args;
861
+ const { runtime, args: kept, project } = runtimeFromArgs(rest);
862
+ requireAuth(runtime);
863
+ const resolvedProject = requireProject(project);
864
+ if (subcommand === "list" || subcommand === "status") {
865
+ const parsedFormat = parseFormat(kept, "markdown");
866
+ const parsedLimit = parseLimit(parsedFormat.args, 20);
867
+ if (parsedLimit.args.length) throw new Error(`unknown fixes ${subcommand} option: ${parsedLimit.args[0]}`);
868
+ const payload = await runTrackedMimCommand(
869
+ runtime,
870
+ { command: `fixes ${subcommand}`, project: resolvedProject, metadata: { format: parsedFormat.format, limit: parsedLimit.limit } },
871
+ () => fetchFixes(runtime, resolvedProject, parsedLimit.limit)
872
+ );
873
+ if (parsedFormat.format === "json") printJson(payload);
874
+ else console.log(formatFixesPayload(payload));
875
+ return;
876
+ }
877
+ if (subcommand === "start") {
878
+ const parsedFormat = parseFormat(kept, "markdown");
879
+ const [identifier, ...unknown] = parsedFormat.args;
880
+ if (!identifier) throw new Error("fixes start requires a rank or finding id");
881
+ if (unknown.length) throw new Error(`unknown fixes start option: ${unknown[0]}`);
882
+ const payload = await runTrackedMimCommand(
883
+ runtime,
884
+ { command: "fixes start", project: resolvedProject, metadata: { format: parsedFormat.format, identifier } },
885
+ async () => {
886
+ const fixes = await fetchFixes(runtime, resolvedProject, 100);
887
+ const item = resolveFixItem(fixes, identifier);
888
+ if (typeof item.finding_index !== "number") throw new Error(`fix ${identifier} is missing a finding index`);
889
+ const path = projectPath(`/api/mim/projects/:project/fixes/${item.finding_index}/start`, resolvedProject);
890
+ return mimRequest(runtime, path, { method: "POST", body: {} });
891
+ }
892
+ );
893
+ if (parsedFormat.format === "json") printJson(payload);
894
+ else console.log(formatFixStartPayload(payload));
895
+ return;
896
+ }
897
+ if (subcommand === "open") {
898
+ const parsedTarget = parseFixTarget(kept);
899
+ const [identifier, ...unknown] = parsedTarget.args;
900
+ if (!identifier) throw new Error("fixes open requires a rank or finding id");
901
+ if (unknown.length) throw new Error(`unknown fixes open option: ${unknown[0]}`);
902
+ const { url } = await runTrackedMimCommand(
903
+ runtime,
904
+ { command: "fixes open", project: resolvedProject, metadata: { identifier, target: parsedTarget.target || "best" } },
905
+ async () => {
906
+ const fixes = await fetchFixes(runtime, resolvedProject, 100);
907
+ const item = resolveFixItem(fixes, identifier);
908
+ const selectedUrl = fixUrl(item, parsedTarget.target, fixes);
909
+ if (!selectedUrl) {
910
+ const target = parsedTarget.target ? `${parsedTarget.target} ` : "";
911
+ throw new Error(`no ${target}URL is available for fix ${identifier}`);
912
+ }
913
+ return { url: selectedUrl };
914
+ }
915
+ );
916
+ openBrowser(url);
917
+ console.log(url);
918
+ return;
919
+ }
920
+ throw new Error(`unknown fixes command: ${subcommand}`);
921
+ }
922
+ function formatBillingStatus(payload) {
923
+ const billing = payload.billing || {};
924
+ const lines = [
925
+ `Billing: ${billing.paid ? "active" : "not active"}`,
926
+ `Project: ${billing.project_key || "(unknown)"}`,
927
+ `Plan: ${billing.plan || "free"}`,
928
+ `Status: ${billing.status || "none"}`,
929
+ `Enforced: ${billing.enforced ? "yes" : "no"}`
930
+ ];
931
+ if (billing.current_period_end) lines.push(`Current period ends: ${billing.current_period_end}`);
932
+ if (billing.cancel_at_period_end) lines.push("Cancel at period end: yes");
933
+ if (billing.checkout_url && !billing.paid) lines.push(`Checkout: ${billing.checkout_url}`);
934
+ if (billing.portal_url && billing.paid) lines.push(`Portal: ${billing.portal_url}`);
935
+ return lines.join("\n");
936
+ }
937
+ async function runBilling(args) {
938
+ const [subcommand = "status", ...rest] = args;
939
+ const { runtime, args: kept, project } = runtimeFromArgs(rest);
940
+ requireAuth(runtime);
941
+ const resolvedProject = requireProject(project);
942
+ if (subcommand === "status") {
943
+ const parsedFormat = parseFormat(kept, "markdown");
944
+ if (parsedFormat.args.length) throw new Error(`unknown billing status option: ${parsedFormat.args[0]}`);
945
+ const payload = await runTrackedMimCommand(
946
+ runtime,
947
+ { command: "billing status", project: resolvedProject, metadata: { format: parsedFormat.format } },
948
+ () => mimRequest(runtime, projectPath("/api/mim/projects/:project/billing", resolvedProject))
949
+ );
950
+ if (parsedFormat.format === "json") printJson(payload);
951
+ else console.log(formatBillingStatus(payload));
952
+ return;
953
+ }
954
+ if (subcommand === "checkout" || subcommand === "open") {
955
+ const parsedFormat = parseFormat(kept, "markdown");
956
+ const [plan = "starter", ...unknown] = parsedFormat.args;
957
+ if (unknown.length) throw new Error(`unknown billing ${subcommand} option: ${unknown[0]}`);
958
+ const payload = await runTrackedMimCommand(
959
+ runtime,
960
+ { command: `billing ${subcommand}`, project: resolvedProject, metadata: { plan, format: parsedFormat.format } },
961
+ () => mimRequest(
962
+ runtime,
963
+ projectPath("/api/mim/projects/:project/billing/checkout", resolvedProject),
964
+ { method: "POST", body: { plan } }
965
+ )
966
+ );
967
+ if (!payload.checkout_url) throw new Error("checkout URL was not returned");
968
+ if (subcommand === "open") openBrowser(payload.checkout_url);
969
+ if (parsedFormat.format === "json") printJson(payload);
970
+ else console.log(payload.checkout_url);
971
+ return;
972
+ }
973
+ if (subcommand === "portal") {
974
+ const parsedFormat = parseFormat(kept, "markdown");
975
+ if (parsedFormat.args.length) throw new Error(`unknown billing portal option: ${parsedFormat.args[0]}`);
976
+ const payload = await runTrackedMimCommand(
977
+ runtime,
978
+ { command: "billing portal", project: resolvedProject, metadata: { format: parsedFormat.format } },
979
+ () => mimRequest(runtime, projectPath("/api/mim/projects/:project/billing", resolvedProject))
980
+ );
981
+ const portalUrl = payload.billing?.portal_url;
982
+ if (!portalUrl) throw new Error("billing portal is not available yet; run `mim billing checkout` first");
983
+ openBrowser(portalUrl);
984
+ if (parsedFormat.format === "json") printJson({ portal_url: portalUrl });
985
+ else console.log(portalUrl);
986
+ return;
987
+ }
988
+ throw new Error(`unknown billing command: ${subcommand}`);
989
+ }
990
+ function claudeMcpInstallCommand(project) {
991
+ return [
992
+ "claude",
993
+ "mcp",
994
+ "add",
995
+ "--transport",
996
+ "stdio",
997
+ "mim",
998
+ "--",
999
+ "npx",
1000
+ "-y",
1001
+ "--package",
1002
+ "@mimeticinc/rerun",
1003
+ "mim-mcp",
1004
+ ...project ? ["--project", project] : []
1005
+ ];
1006
+ }
1007
+ function codexMcpConfig(project) {
1008
+ const args = ["-y", "--package", "@mimeticinc/rerun", "mim-mcp", ...project ? ["--project", project] : []];
1009
+ return `[mcp_servers.mim]
1010
+ command = "npx"
1011
+ args = ${JSON.stringify(args)}
1012
+ `;
1013
+ }
1014
+ async function runMcp(args) {
1015
+ const [subcommand, target, ...rest] = args;
1016
+ const { project, args: kept } = runtimeFromArgs(rest);
1017
+ if (!subcommand) {
1018
+ console.log("Use `mim mcp install claude` or `mim mcp install codex`.");
1019
+ console.log("The MCP server exposes project-scoped growth context and recording review briefs without raw replay event dumps.");
1020
+ return;
1021
+ }
1022
+ if (subcommand !== "install") throw new Error(`unknown mcp command: ${subcommand}`);
1023
+ let runInstall = false;
1024
+ for (const arg of kept) {
1025
+ if (arg === "--run") runInstall = true;
1026
+ else throw new Error(`unknown mcp install option: ${arg}`);
1027
+ }
1028
+ if (target === "claude") {
1029
+ const command = claudeMcpInstallCommand(project);
1030
+ if (!runInstall) {
1031
+ console.log(command.map((part) => /\s/.test(part) ? JSON.stringify(part) : part).join(" "));
1032
+ return;
1033
+ }
1034
+ await new Promise((resolve, reject) => {
1035
+ const child = spawn(command[0], command.slice(1), { stdio: "inherit" });
1036
+ child.on("error", reject);
1037
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`claude mcp add failed with exit code ${code}`)));
1038
+ });
1039
+ return;
1040
+ }
1041
+ if (target === "codex") {
1042
+ console.log(codexMcpConfig(project));
1043
+ return;
1044
+ }
1045
+ throw new Error("mcp install target must be claude or codex");
1046
+ }
1047
+ function resolveIosAuditToolDir() {
1048
+ const configured = process.env.MIM_IOS_AUDIT_TOOL_DIR?.trim();
1049
+ if (configured) return configured;
1050
+ const home = homeDir();
1051
+ const candidates = [
1052
+ join(home, "pet-animator", "ios-audit-tool"),
1053
+ join(home, "ios-audit-tool")
1054
+ ];
1055
+ for (const candidate of candidates) {
1056
+ if (existsSync(candidate)) return candidate;
1057
+ }
1058
+ throw new Error(
1059
+ "ios-audit-tool directory not found. Set MIM_IOS_AUDIT_TOOL_DIR or clone it to ~/ios-audit-tool"
1060
+ );
1061
+ }
1062
+ function spawnLocalTool(command, args, cwd) {
1063
+ return new Promise((resolve, reject) => {
1064
+ const child = spawn(command, args, { stdio: "inherit", cwd });
1065
+ child.on(
1066
+ "error",
1067
+ (err) => reject(new Error(`failed to start ${command}: ${err.message}`))
1068
+ );
1069
+ child.on(
1070
+ "close",
1071
+ (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`))
1072
+ );
1073
+ });
1074
+ }
1075
+ async function runWebAudit(args) {
1076
+ const toolDir = resolveIosAuditToolDir();
1077
+ const scriptPath = join(toolDir, "web_auditor.py");
1078
+ if (!existsSync(scriptPath)) throw new Error(`web_auditor.py not found at ${scriptPath}`);
1079
+ let url = "";
1080
+ const passthrough = [];
1081
+ for (let i = 0; i < args.length; i += 1) {
1082
+ const arg = args[i];
1083
+ if (arg === "--output" || arg === "-o") {
1084
+ const [value, next] = readValue(args, i, arg);
1085
+ passthrough.push("--output", value);
1086
+ i = next;
1087
+ } else if (arg === "--deep") {
1088
+ passthrough.push("--deep");
1089
+ } else if (arg === "--batch") {
1090
+ const [value, next] = readValue(args, i, arg);
1091
+ passthrough.push("--batch", value);
1092
+ i = next;
1093
+ } else if (arg === "--help" || arg === "-h") {
1094
+ passthrough.push("--help");
1095
+ } else if (!url && !arg.startsWith("-")) {
1096
+ url = arg;
1097
+ } else {
1098
+ passthrough.push(arg);
1099
+ }
1100
+ }
1101
+ const pyArgs = [scriptPath];
1102
+ if (url) pyArgs.push(url);
1103
+ pyArgs.push(...passthrough);
1104
+ if (!url && !passthrough.includes("--batch") && !passthrough.includes("--help")) {
1105
+ throw new Error("web-audit requires a URL or --batch <file>");
1106
+ }
1107
+ console.log(`Running web_auditor.py${url ? ` on ${url}` : ""}...`);
1108
+ await spawnLocalTool("python3", pyArgs, toolDir);
1109
+ }
1110
+ async function runIpaAudit(args) {
1111
+ const toolDir = resolveIosAuditToolDir();
1112
+ const scriptPath = join(toolDir, "ipa_analyzer.py");
1113
+ if (!existsSync(scriptPath)) throw new Error(`ipa_analyzer.py not found at ${scriptPath}`);
1114
+ const paths = [];
1115
+ const passthrough = [];
1116
+ for (const arg of args) {
1117
+ if (arg === "--help" || arg === "-h") {
1118
+ passthrough.push("--help");
1119
+ } else if (!arg.startsWith("-")) {
1120
+ paths.push(arg);
1121
+ } else {
1122
+ passthrough.push(arg);
1123
+ }
1124
+ }
1125
+ if (!paths.length && !passthrough.includes("--help")) {
1126
+ throw new Error("ipa-audit requires one or more .ipa file paths");
1127
+ }
1128
+ const pyArgs = [scriptPath, ...paths, ...passthrough];
1129
+ console.log(`Running ipa_analyzer.py on ${paths.join(", ") || "..."}...`);
1130
+ await spawnLocalTool("python3", pyArgs, toolDir);
1131
+ }
1132
+ async function mimMain(argv = process.argv.slice(2)) {
1133
+ const [command, ...args] = argv;
1134
+ if (!command || command === "-h" || command === "--help") {
1135
+ console.log(mimUsage());
1136
+ return;
1137
+ }
1138
+ if (command === "auth") return runAuth(args);
1139
+ if (command === "projects") return runProjects(args);
1140
+ if (command === "context") return runContext(args);
1141
+ if (command === "recordings") return runCollectionCommand("recordings", args);
1142
+ if (command === "replay") return runReplay(args);
1143
+ if (command === "findings") return runCollectionCommand("findings", args);
1144
+ if (command === "audit") return runAudit(args);
1145
+ if (command === "web-audit") return runWebAudit(args);
1146
+ if (command === "ipa-audit") return runIpaAudit(args);
1147
+ if (command === "fixes") return runFixes(args);
1148
+ if (command === "billing") return runBilling(args);
1149
+ if (command === "mcp") return runMcp(args);
1150
+ throw new Error(`unknown command: ${command}`);
1151
+ }
1152
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1153
+ mimMain().catch((error) => {
1154
+ console.error(`mim: ${error instanceof Error ? error.message : String(error)}`);
1155
+ process.exit(1);
1156
+ });
1157
+ }
1158
+
1159
+ export {
1160
+ mimUsage,
1161
+ mimConfigDir,
1162
+ mimConfigPath,
1163
+ loadMimConfig,
1164
+ saveMimConfig,
1165
+ clearMimConfig,
1166
+ normalizeMimApiBaseUrl,
1167
+ mimApiBaseUrl,
1168
+ mimAuthToken,
1169
+ defaultMimProject,
1170
+ shouldSendMimTelemetry,
1171
+ sanitizeMimMetadata,
1172
+ buildMimUsageEvent,
1173
+ trackMimUsageEvent,
1174
+ formatMimListPayload,
1175
+ formatRecordingsPayload,
1176
+ formatReplayInsightPayload,
1177
+ formatFixesPayload,
1178
+ claudeMcpInstallCommand,
1179
+ codexMcpConfig,
1180
+ mimMain
1181
+ };