@justmpm/mmx-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,203 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tool: mmx_video_generate (ASYNC)
3
+ *
4
+ * Gera vídeo via MiniMax Hailuo 2.3 com polling interno.
5
+ * Salva no outputDir do user. Retorna resource_link (.mp4 via file URI).
6
+ *
7
+ * Notes (notebook MiniMax):
8
+ * - Video is strictly async. API returns task_id; poll every 10s.
9
+ * - file_id expires after 9h. We download immediately on Success.
10
+ * - Rate limit: 5 RPM.
11
+ * - Costs: $0.19-$0.49 per 6s video depending on resolution and model.
12
+ * - Polling aborta se o cliente MCP desconectar (AbortController).
13
+ */
14
+ import { z } from "zod";
15
+ import { handleToolError, runMmx, runMmxVideoWithPolling, validateInputFile, validateOutputDir, } from "../utils.js";
16
+ import { resourceFromPath, textContent } from "../media.js";
17
+ // =============================================================================
18
+ // Definição da Tool
19
+ // =============================================================================
20
+ export const name = "mmx_video_generate";
21
+ export const description = "Gera um vídeo a partir de prompt usando MiniMax Hailuo 2.3. " +
22
+ "**outputDir é OBRIGATÓRIO** — escolha onde salvar (ex: ~/Videos/mmx). " +
23
+ "Retorna resource_link (.mp4 via file URI). " +
24
+ "**Rate limit: 5 RPM. Custo: $0.19-$0.49 por vídeo (6s)** — operação MAIS CARA do pacote. " +
25
+ "Duração típica: 30s-3min (polling interno a cada 10s). " +
26
+ "file_id expira em 9h — MCP baixa IMEDIATAMENTE. " +
27
+ "Modelos: MiniMax-Hailuo-2.3 (qualidade 1080p, ~$0.49) ou -Fast (velocidade 768p, ~$0.19). " +
28
+ "Se timeout (>5min), use mmx_video_task_get com o taskId retornado. " +
29
+ "Se o cliente MCP desconectar durante polling, operação cancela (AbortController).";
30
+ export const inputSchema = z.object({
31
+ outputDir: z
32
+ .string()
33
+ .min(1)
34
+ .describe("Diretório ABSOLUTO onde salvar o vídeo. Obrigatório."),
35
+ prompt: z
36
+ .string()
37
+ .min(1)
38
+ .describe("Descrição do vídeo. Quanto mais detalhada, melhor a aderência."),
39
+ model: z
40
+ .enum(["MiniMax-Hailuo-2.3", "MiniMax-Hailuo-2.3-Fast", "MiniMax-Hailuo-02"])
41
+ .optional()
42
+ .describe("Modelo. Default: MiniMax-Hailuo-2.3 (qualidade). Use Fast pra velocidade/custo."),
43
+ firstFrame: z
44
+ .string()
45
+ .optional()
46
+ .describe("Path ou URL da imagem do primeiro frame. Validado contra path traversal."),
47
+ callbackUrl: z
48
+ .string()
49
+ .url()
50
+ .optional()
51
+ .describe("Webhook URL externo. **ATENÇÃO**: dados do vídeo são enviados para este servidor."),
52
+ maxWaitMs: z
53
+ .number()
54
+ .int()
55
+ .positive()
56
+ .optional()
57
+ .describe("Tempo máximo de espera total em ms. Default 300_000 (5min)."),
58
+ pollIntervalMs: z
59
+ .number()
60
+ .int()
61
+ .positive()
62
+ .optional()
63
+ .describe("Intervalo de polling em ms. Default 10_000 (10s conforme notebook MiniMax)."),
64
+ });
65
+ // =============================================================================
66
+ // Handler
67
+ // =============================================================================
68
+ export async function handler(args) {
69
+ const parsed = inputSchema.safeParse(args);
70
+ if (!parsed.success) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Erro de validação: ${parsed.error.issues
76
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
77
+ .join("; ")}`,
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ const data = parsed.data;
84
+ // Validação do outputDir
85
+ const dirCheck = validateOutputDir(data.outputDir);
86
+ if (!dirCheck.ok) {
87
+ return {
88
+ content: [{ type: "text", text: `Erro no outputDir: ${dirCheck.error}` }],
89
+ isError: true,
90
+ };
91
+ }
92
+ // Validação de segurança do firstFrame (path traversal defense)
93
+ if (data.firstFrame && !data.firstFrame.startsWith("http")) {
94
+ const frameCheck = validateInputFile(data.firstFrame);
95
+ if (!frameCheck.ok) {
96
+ return {
97
+ content: [{ type: "text", text: `Erro no firstFrame: ${frameCheck.error}` }],
98
+ isError: true,
99
+ };
100
+ }
101
+ }
102
+ const cliArgs = ["video", "generate", "--prompt", data.prompt];
103
+ if (data.model)
104
+ cliArgs.push("--model", data.model);
105
+ if (data.firstFrame)
106
+ cliArgs.push("--first-frame", data.firstFrame);
107
+ if (data.callbackUrl)
108
+ cliArgs.push("--callback-url", data.callbackUrl);
109
+ // AbortController local — preparado para integração futura com
110
+ // `notifications/cancelled` do MCP SDK (v1+). Hoje, o timeout do polling
111
+ // (maxWaitMs) é a única forma de cancelamento. Mantemos o controller
112
+ // criado para que extensões futuras (signal externo via progresso MCP)
113
+ // possam plugar aqui sem refator estrutural.
114
+ const abortController = new AbortController();
115
+ try {
116
+ const { downloadPath, taskInfo } = await runMmxVideoWithPolling(cliArgs, {
117
+ maxWaitMs: data.maxWaitMs,
118
+ pollIntervalMs: data.pollIntervalMs,
119
+ outputDir: dirCheck.resolved,
120
+ signal: abortController.signal,
121
+ });
122
+ try {
123
+ const resource = resourceFromPath(downloadPath, "video/mp4", `Vídeo gerado (task ${taskInfo.task_id})`);
124
+ return { content: [resource] };
125
+ }
126
+ catch (err) {
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ return {
129
+ content: [
130
+ textContent(`Vídeo gerado mas arquivo não encontrado em ${downloadPath}: ${msg}`),
131
+ ],
132
+ isError: true,
133
+ };
134
+ }
135
+ }
136
+ catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ // Mensagem de timeout do runMmxVideoWithPolling já inclui "Use a tool mmx_video_task_get"
139
+ // e é a melhor mensagem para esse caso — preserva ela.
140
+ if (msg.toLowerCase().includes("timeout") || msg.toLowerCase().includes("cancelada")) {
141
+ return { content: [{ type: "text", text: msg }], isError: true };
142
+ }
143
+ return handleToolError("gerar vídeo", { stdout: "", stderr: msg, exitCode: 1 });
144
+ }
145
+ }
146
+ // =============================================================================
147
+ // Tool auxiliar: mmx_video_task_get (polling manual)
148
+ // =============================================================================
149
+ export const taskGetName = "mmx_video_task_get";
150
+ export const taskGetDescription = "Consulta o status de uma task de geração de vídeo manualmente. " +
151
+ "Use apenas se mmx_video_generate retornou timeout e você quer continuar o polling. " +
152
+ "O caso normal: mmx_video_generate já faz polling automático.";
153
+ export const taskGetInputSchema = z.object({
154
+ taskId: z.string().describe("ID da task de vídeo (UUID-like)."),
155
+ });
156
+ export async function taskGetHandler(args) {
157
+ const parsed = taskGetInputSchema.safeParse(args);
158
+ if (!parsed.success) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: `Erro de validação: ${parsed.error.issues
164
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
165
+ .join("; ")}`,
166
+ },
167
+ ],
168
+ isError: true,
169
+ };
170
+ }
171
+ const { taskId } = parsed.data;
172
+ const result = await runMmx([
173
+ "video",
174
+ "task",
175
+ "get",
176
+ "--task-id",
177
+ taskId,
178
+ "--output",
179
+ "json",
180
+ ]);
181
+ if (result.exitCode !== 0) {
182
+ return handleToolError("consultar task de vídeo", result);
183
+ }
184
+ return { content: [{ type: "text", text: result.stdout }] };
185
+ }
186
+ // =============================================================================
187
+ // Aggregate export
188
+ // =============================================================================
189
+ export const videoTools = [
190
+ {
191
+ name: "mmx_video_generate",
192
+ description,
193
+ inputSchema,
194
+ handler,
195
+ },
196
+ {
197
+ name: "mmx_video_task_get",
198
+ description: taskGetDescription,
199
+ inputSchema: taskGetInputSchema,
200
+ handler: taskGetHandler,
201
+ },
202
+ ];
203
+ //# sourceMappingURL=video.js.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tipos compartilhados
3
+ *
4
+ * Strategy: reusa os tipos oficiais do MCP SDK ao invés de redefinir.
5
+ * `ToolResult`, `TextContent`, `ImageContent`, `AudioContent` e `ResourceLink`
6
+ * vêm direto do SDK, garantindo compatibilidade exata.
7
+ */
8
+ import type { AudioContent, CallToolResult, ImageContent, ResourceLink, TextContent } from "@modelcontextprotocol/sdk/types.js";
9
+ import type { ZodTypeAny } from "zod";
10
+ /** Resultado de uma chamada de tool. Alias do tipo oficial do MCP SDK. */
11
+ export type ToolResult = CallToolResult;
12
+ /** Content block de texto. */
13
+ export type { TextContent, ImageContent, AudioContent, ResourceLink };
14
+ /** União de todos os content types que podemos retornar. */
15
+ export type ToolContent = TextContent | ImageContent | AudioContent | ResourceLink;
16
+ /**
17
+ * Interface padrão de toda tool MCP exposta por este servidor.
18
+ *
19
+ * Cada módulo de domínio exporta `*Tools: McpTool[]` que o servidor concatena.
20
+ */
21
+ export interface McpTool {
22
+ /** Nome único (ex: 'mmx_login'). */
23
+ name: string;
24
+ /** Descrição legível — vai pra `description` no listTools. */
25
+ description: string;
26
+ /** Input schema (Zod). Convertido pra JSON Schema via `z.toJSONSchema()`. */
27
+ inputSchema: ZodTypeAny;
28
+ /** Handler que recebe `unknown` (clientes podem mandar o que quiser). */
29
+ handler: (args: unknown) => Promise<ToolResult>;
30
+ }
31
+ /**
32
+ * Resultado retornado pelo helper runMmx após executar um comando do CLI.
33
+ */
34
+ export interface SpawnResult {
35
+ /** Saída padrão (stdout) */
36
+ stdout: string;
37
+ /** Saída de erro (stderr) */
38
+ stderr: string;
39
+ /** Código de saída (0 = sucesso) */
40
+ exitCode: number;
41
+ }
42
+ /**
43
+ * Como invocar o binário do mmx. Resolvido no boot via `resolveMmxInvocation`.
44
+ *
45
+ * - `command: "mmx"` → uso direto (PATH)
46
+ * - `command: "npx"` → `prefixArgs` carrega `-y mmx-cli mmx` (fallback)
47
+ */
48
+ export interface MmxInvocation {
49
+ command: "mmx" | "npx";
50
+ prefixArgs: string[];
51
+ }
52
+ /**
53
+ * Opções do `runMmx`.
54
+ */
55
+ export interface RunMmxOptions {
56
+ /** Working directory. Default: process.cwd(). */
57
+ cwd?: string;
58
+ /** Variáveis de ambiente extras (herda do process.env por padrão). */
59
+ env?: NodeJS.ProcessEnv;
60
+ /**
61
+ * Timeout em ms. Default 300_000 (5min).
62
+ * Use `0` para desabilitar.
63
+ */
64
+ timeoutMs?: number;
65
+ }
66
+ /** Status retornado por `mmx video task get --output json`. */
67
+ export type VideoTaskStatus = "Processing" | "Success" | "Fail" | "Expired";
68
+ /** Resposta parseada de `mmx video task get --output json`. */
69
+ export interface VideoTaskInfo {
70
+ task_id: string;
71
+ status: VideoTaskStatus;
72
+ file_id?: string;
73
+ error_message?: string;
74
+ }
75
+ /**
76
+ * Opções para `runMmxVideoWithPolling`.
77
+ */
78
+ export interface VideoPollingOptions {
79
+ /** Tempo máximo de espera total em ms. Default 300_000 (5min). */
80
+ maxWaitMs?: number;
81
+ /** Intervalo de polling em ms. Default 10_000 (10s — nota MiniMax). */
82
+ pollIntervalMs?: number;
83
+ }
84
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tipos compartilhados
3
+ *
4
+ * Strategy: reusa os tipos oficiais do MCP SDK ao invés de redefinir.
5
+ * `ToolResult`, `TextContent`, `ImageContent`, `AudioContent` e `ResourceLink`
6
+ * vêm direto do SDK, garantindo compatibilidade exata.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,220 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Utilitários compartilhados
3
+ *
4
+ * Helpers para executar comandos do CLI `mmx` via child_process:
5
+ * - resolveMmxInvocation(): estratégia em cascata (mmx global → npx)
6
+ * - runMmx(): spawn seguro com timeout
7
+ * - isAuthError(): detecção de erros de autenticação
8
+ * - isQuotaError(): detecção de erro de quota
9
+ * - checkMmxVersion(): health check no boot
10
+ * - Mensagens de erro padronizadas
11
+ * - runMmxVideoWithPolling(): polling async para vídeo com timeout
12
+ * - Validação de paths (security: path traversal, realpath, size)
13
+ * - Sanitização de erros (redact API keys)
14
+ * - Concurrency limiter
15
+ */
16
+ import type { MmxInvocation, RunMmxOptions, SpawnResult, VideoPollingOptions, VideoTaskInfo } from "./types.js";
17
+ /**
18
+ * Procura um comando no PATH. Cross-platform sem dependência externa.
19
+ * - Windows: usa `where <cmd>` (via shell para resolver .cmd/.bat).
20
+ * - Unix: usa `command -v <cmd>` (POSIX).
21
+ *
22
+ * @returns Path absoluto do executável, ou `null` se não encontrado.
23
+ */
24
+ export declare function findInPath(command: string): string | null;
25
+ /**
26
+ * Determina como invocar o `mmx`. Estratégia em cascata:
27
+ * 1. `mmx` no PATH global (caso o cliente MCP tenha npm globals no PATH)
28
+ * 2. `npx -y mmx-cli` (sempre funciona em qualquer máquina com Node ≥ 18)
29
+ *
30
+ * O resultado é cacheado em memória para evitar resolução repetida.
31
+ */
32
+ export declare function resolveMmxInvocation(): MmxInvocation;
33
+ /**
34
+ * Reseta o cache de invocação. Útil para testes.
35
+ */
36
+ export declare function _resetMmxInvocationCache(): void;
37
+ /**
38
+ * Executa um comando do CLI `mmx` e retorna stdout, stderr e exitCode.
39
+ *
40
+ * Usa child_process.spawn (NÃO exec) por causa do limite de 1MB do exec.
41
+ * Suporta AbortSignal para cancelamento cooperativo.
42
+ * Limitado a 3 execuções simultâneas via `mmxConcurrencyLimiter`.
43
+ *
44
+ * @param args - Argumentos para o mmx (ex: ["image", "generate", "--prompt", "..."])
45
+ * @param options - cwd, env, timeoutMs (default 300_000 = 5min), signal (AbortSignal)
46
+ */
47
+ export declare function runMmx(args: string[], options?: RunMmxOptions & {
48
+ signal?: AbortSignal;
49
+ }): Promise<SpawnResult>;
50
+ /**
51
+ * Implementação interna de `runMmx` (sem concurrency limit).
52
+ * Usada por `runMmx` (com limit) e por testes.
53
+ */
54
+ export declare function runMmxInner(args: string[], options?: RunMmxOptions & {
55
+ signal?: AbortSignal;
56
+ }): Promise<SpawnResult>;
57
+ /**
58
+ * Submete um vídeo para geração async, faz polling até conclusão, e
59
+ * baixa o vídeo IMEDIATAMENTE (file_id expira em 9h conforme notebook MiniMax).
60
+ *
61
+ * Retorna:
62
+ * - downloadPath: caminho do vídeo baixado em disco.
63
+ *
64
+ * Lança erro com mensagem amigável se timeout, fail, expired ou cancelamento.
65
+ *
66
+ * @param options.outputDir - Diretório onde salvar o vídeo (obrigatório)
67
+ * @param options.signal - AbortSignal para cancelamento cooperativo
68
+ */
69
+ export declare function runMmxVideoWithPolling(args: string[], options?: VideoPollingOptions & {
70
+ outputDir: string;
71
+ signal?: AbortSignal;
72
+ }): Promise<{
73
+ downloadPath: string;
74
+ taskInfo: VideoTaskInfo;
75
+ }>;
76
+ export declare function isAuthError(message: string): boolean;
77
+ export declare function isQuotaError(message: string): boolean;
78
+ /**
79
+ * Detecta se o erro é devido ao CLI mmx não estar instalado.
80
+ * Útil para transformar `spawn ENOENT` em mensagem acionável.
81
+ */
82
+ export declare function isCliNotInstalled(message: string): boolean;
83
+ /**
84
+ * Detecta timeout (já é um erro conhecido).
85
+ */
86
+ export declare function isTimeoutError(message: string): boolean;
87
+ /**
88
+ * Detecta se o erro é "arquivo não existe" — comum em upload, vision, etc.
89
+ */
90
+ export declare function isFileNotFound(message: string): boolean;
91
+ /**
92
+ * Mensagem para erros de autenticação. Direciona para mmx_login
93
+ * e explica a diferença entre Pay-as-you-go e Token Plan.
94
+ */
95
+ export declare function authErrorMessage(detail: string): string;
96
+ /**
97
+ * Mensagem genérica — fallback usado quando nenhum pattern específico casa.
98
+ * Agora inclui instrução genérica de correção.
99
+ */
100
+ export declare function genericErrorMessage(operation: string, detail: string): string;
101
+ /**
102
+ * Mensagem específica quando o CLI mmx não está instalado.
103
+ * Guia o usuário a instalar.
104
+ */
105
+ export declare function cliNotInstalledMessage(operation: string, detail: string): string;
106
+ /**
107
+ * Mensagem específica para quota excedida — com janela de reset e alternativas.
108
+ */
109
+ export declare function quotaErrorMessage(operation: string, detail: string): string;
110
+ /**
111
+ * Mensagem específica para timeout.
112
+ */
113
+ export declare function timeoutErrorMessage(operation: string, detail: string): string;
114
+ /**
115
+ * Mensagem específica para arquivo não encontrado.
116
+ */
117
+ export declare function fileNotFoundMessage(operation: string, filePath: string, detail: string): string;
118
+ /**
119
+ * Helper central para tratamento de erros em handlers de tool.
120
+ * Detecta automaticamente:
121
+ * - mmx não instalado → cliNotInstalledMessage
122
+ * - autenticação → authErrorMessage
123
+ * - quota/rate limit → quotaErrorMessage
124
+ * - timeout → timeoutErrorMessage
125
+ * - arquivo não encontrado → fileNotFoundMessage
126
+ * - genérico → genericErrorMessage
127
+ *
128
+ * Uso:
129
+ * ```ts
130
+ * const result = await runMmx(["image", "generate", "--prompt", prompt]);
131
+ * if (result.exitCode !== 0) {
132
+ * return handleToolError("gerar imagem", result);
133
+ * }
134
+ * ```
135
+ */
136
+ export declare function handleToolError(operation: string, result: SpawnResult, options?: {
137
+ filePath?: string;
138
+ }): {
139
+ content: Array<{
140
+ type: "text";
141
+ text: string;
142
+ }>;
143
+ isError: true;
144
+ };
145
+ export interface VersionCheckResult {
146
+ installed: string | null;
147
+ minimum: string;
148
+ ok: boolean;
149
+ message: string;
150
+ }
151
+ /**
152
+ * Compara duas versões semânticas (X.Y.Z). Retorna negativo se a < b.
153
+ */
154
+ export declare function compareVersions(a: string, b: string): number;
155
+ /**
156
+ * Roda `mmx --version` e extrai X.Y.Z.
157
+ * Retorna `installed: null` se mmx não está disponível.
158
+ */
159
+ export declare function checkMmxVersion(): Promise<VersionCheckResult>;
160
+ /** Tamanho máximo padrão de arquivo de entrada (10MB). Acima disso, rejeitar. */
161
+ export declare const MAX_INPUT_FILE_BYTES: number;
162
+ /** Resultado da validação de path. */
163
+ export interface PathValidationResult {
164
+ ok: boolean;
165
+ /** Path absoluto resolvido (com realpath se arquivo existe, senão path resolvido). */
166
+ resolved: string;
167
+ /** Tamanho em bytes (apenas para arquivos). */
168
+ size?: number;
169
+ /** Mensagem de erro caso !ok. */
170
+ error?: string;
171
+ }
172
+ /**
173
+ * Valida um diretório de saída. Cria se não existir. Defesa contra path traversal.
174
+ *
175
+ * @param dir - Path do diretório (relativo ou absoluto)
176
+ * @param create - Se true, cria o diretório se não existir (default: true)
177
+ * @returns PathValidationResult com resolved path ou error
178
+ */
179
+ export declare function validateOutputDir(dir: string, create?: boolean): PathValidationResult;
180
+ /**
181
+ * Valida um arquivo de entrada. Defesa contra:
182
+ * - Path traversal (`../../../etc/passwd`)
183
+ * - Device files (`/dev/zero`, `\\.\NUL`)
184
+ * - Arquivos excessivamente grandes (>MAX_INPUT_FILE_BYTES)
185
+ *
186
+ * @param filePath - Path do arquivo
187
+ * @param maxBytes - Limite custom (default: MAX_INPUT_FILE_BYTES = 10MB)
188
+ */
189
+ export declare function validateInputFile(filePath: string, maxBytes?: number): PathValidationResult;
190
+ /**
191
+ * Redacta possíveis API keys em uma mensagem de erro antes de retornar pro LLM.
192
+ * Defesa contra keys vazadas via stderr do CLI.
193
+ */
194
+ export declare function sanitizeErrorMessage(message: string): string;
195
+ /**
196
+ * Semáforo simples para limitar número de execuções simultâneas do CLI `mmx`.
197
+ * Evita DoS local (10 tools em paralelo = 10 processos simultâneos).
198
+ *
199
+ * IMPORTANTE: `acquire()` rejeita após `queueTimeoutMs` para evitar
200
+ * tools travando indefinidamente em fila cheia.
201
+ */
202
+ declare class ConcurrencyLimiter {
203
+ private active;
204
+ private readonly max;
205
+ private readonly queue;
206
+ private readonly queueTimeoutMs;
207
+ constructor(max: number, queueTimeoutMs?: number);
208
+ acquire(): Promise<void>;
209
+ release(): void;
210
+ }
211
+ /** Limiter global — max 3 chamadas simultâneas ao CLI `mmx`. */
212
+ export declare const mmxConcurrencyLimiter: ConcurrencyLimiter;
213
+ /**
214
+ * Helper: executa uma função sob o limiter de concorrência.
215
+ * Garante release() mesmo em caso de throw.
216
+ * Se acquire() for rejeitado por timeout de fila, propaga o erro.
217
+ */
218
+ export declare function withConcurrency<T>(fn: () => Promise<T>): Promise<T>;
219
+ export {};
220
+ //# sourceMappingURL=utils.d.ts.map