@mixmake/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.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # MixMake CLI (MVP)
2
+
3
+ Thin wrapper around MixMake MCP for the happy path.
4
+
5
+ ## Run locally
6
+
7
+ ```bash
8
+ node cli/src/index.js doctor
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ```bash
14
+ mixmake doctor
15
+ mixmake key --email you@example.com
16
+ mixmake transcribe --audio-url https://example.com/file.mp3
17
+ mixmake transcribe --audio-id <audio_id>
18
+ mixmake edit --transcript-id <id> --delete id1,id2,id3
19
+ mixmake render --transcript-id <id>
20
+ ```
21
+
22
+ ## Config
23
+
24
+ Saved at:
25
+
26
+ `~/.config/mixmake/config.json`
27
+
28
+ Shape:
29
+
30
+ ```json
31
+ {
32
+ "endpoint": "https://mixmake.com/api/mcp",
33
+ "apiKey": "mmk_..."
34
+ }
35
+ ```
36
+
37
+ Environment overrides:
38
+
39
+ - `MIXMAKE_MCP_URL`
40
+ - `MIXMAKE_API_KEY`
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mixmake/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "MixMake CLI for MCP happy-path workflows",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/swappysh/mixmake.git",
10
+ "directory": "cli"
11
+ },
12
+ "homepage": "https://mixmake.com/for-agents",
13
+ "bin": {
14
+ "mixmake": "src/index.js"
15
+ },
16
+ "scripts": {
17
+ "start": "node ./src/index.js",
18
+ "doctor": "node ./src/index.js doctor"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ }
23
+ }
package/src/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import { homedir } from 'node:os';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_ENDPOINT = 'https://mixmake.com/api/mcp';
6
+ const CONFIG_DIR = path.join(homedir(), '.config', 'mixmake');
7
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
8
+
9
+ export async function loadConfig() {
10
+ try {
11
+ const raw = await readFile(CONFIG_PATH, 'utf8');
12
+ const parsed = JSON.parse(raw);
13
+ return {
14
+ endpoint: parsed.endpoint || DEFAULT_ENDPOINT,
15
+ apiKey: parsed.apiKey || null,
16
+ };
17
+ } catch {
18
+ return {
19
+ endpoint: DEFAULT_ENDPOINT,
20
+ apiKey: null,
21
+ };
22
+ }
23
+ }
24
+
25
+ export async function saveConfig(next) {
26
+ await mkdir(CONFIG_DIR, { recursive: true });
27
+ await writeFile(CONFIG_PATH, JSON.stringify(next, null, 2) + '\n', 'utf8');
28
+ }
29
+
30
+ export function withEnvOverrides(config) {
31
+ return {
32
+ endpoint: process.env.MIXMAKE_MCP_URL || config.endpoint,
33
+ apiKey: process.env.MIXMAKE_API_KEY || config.apiKey,
34
+ };
35
+ }
36
+
37
+ export function getConfigPath() {
38
+ return CONFIG_PATH;
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import { callTool, extractToolText, initialize } from './mcpClient.js';
3
+ import { getConfigPath, loadConfig, saveConfig, withEnvOverrides } from './config.js';
4
+
5
+ function parseArgs(argv) {
6
+ const positionals = [];
7
+ const flags = {};
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const cur = argv[i];
10
+ if (!cur.startsWith('--')) {
11
+ positionals.push(cur);
12
+ continue;
13
+ }
14
+ const key = cur.slice(2);
15
+ const next = argv[i + 1];
16
+ if (!next || next.startsWith('--')) {
17
+ flags[key] = true;
18
+ continue;
19
+ }
20
+ flags[key] = next;
21
+ i += 1;
22
+ }
23
+ return { positionals, flags };
24
+ }
25
+
26
+ function usage() {
27
+ console.log(`mixmake <command> [flags]
28
+
29
+ Commands:
30
+ doctor
31
+ key --email <email>
32
+ transcribe --audio-url <url> | --audio-id <id> [--label <label>]
33
+ edit --transcript-id <id> --delete <id1,id2,...>
34
+ render --transcript-id <id>
35
+
36
+ Config:
37
+ ${getConfigPath()}
38
+ Environment overrides:
39
+ MIXMAKE_MCP_URL, MIXMAKE_API_KEY
40
+ `);
41
+ }
42
+
43
+ function printResult(label, data) {
44
+ console.log(`\n${label}:`);
45
+ console.log(JSON.stringify(data, null, 2));
46
+ }
47
+
48
+ async function main() {
49
+ const { positionals, flags } = parseArgs(process.argv.slice(2));
50
+ const cmd = positionals[0];
51
+ if (!cmd || cmd === 'help' || cmd === '--help') {
52
+ usage();
53
+ process.exit(0);
54
+ }
55
+
56
+ const config = withEnvOverrides(await loadConfig());
57
+ const endpoint = config.endpoint;
58
+
59
+ if (cmd === 'doctor') {
60
+ const init = await initialize(endpoint);
61
+ printResult('doctor', {
62
+ ok: true,
63
+ endpoint,
64
+ session_id: init.sessionId,
65
+ request_id: init.requestId || null,
66
+ server_info: init.payload?.result?.serverInfo || null,
67
+ timestamp: new Date().toISOString(),
68
+ });
69
+ return;
70
+ }
71
+
72
+ if (cmd === 'key') {
73
+ const email = flags.email;
74
+ if (!email || typeof email !== 'string') throw new Error('--email is required');
75
+ const init = await initialize(endpoint);
76
+ const out = await callTool({
77
+ endpoint,
78
+ sessionId: init.sessionId,
79
+ token: init.token,
80
+ apiKey: config.apiKey,
81
+ name: 'get_api_key',
82
+ args: { email },
83
+ });
84
+ const text = extractToolText(out.payload);
85
+ printResult('get_api_key', text ? JSON.parse(text) : out.payload);
86
+
87
+ try {
88
+ const parsed = text ? JSON.parse(text) : null;
89
+ const key = parsed?.api_key || parsed?.key || null;
90
+ if (key && !process.env.MIXMAKE_API_KEY) {
91
+ await saveConfig({ endpoint, apiKey: key });
92
+ console.log(`\nSaved api_key to ${getConfigPath()}`);
93
+ }
94
+ } catch {
95
+ // no-op: only auto-save if response shape is predictable JSON
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (cmd === 'transcribe') {
101
+ const audioUrl = flags['audio-url'];
102
+ const audioId = flags['audio-id'];
103
+ const label = flags.label;
104
+ if (!audioUrl && !audioId) throw new Error('Provide --audio-url or --audio-id');
105
+ const init = await initialize(endpoint);
106
+ const out = await callTool({
107
+ endpoint,
108
+ sessionId: init.sessionId,
109
+ token: init.token,
110
+ apiKey: config.apiKey,
111
+ name: 'transcribe_audio',
112
+ args: { audio_url: audioUrl, audio_id: audioId, label },
113
+ });
114
+ const text = extractToolText(out.payload);
115
+ printResult('transcribe_audio', text ? JSON.parse(text) : out.payload);
116
+ return;
117
+ }
118
+
119
+ if (cmd === 'edit') {
120
+ const transcriptId = flags['transcript-id'];
121
+ const deleted = flags.delete;
122
+ if (!transcriptId || typeof transcriptId !== 'string') throw new Error('--transcript-id is required');
123
+ if (!deleted || typeof deleted !== 'string') throw new Error('--delete is required (comma-separated)');
124
+ const deletedIds = deleted
125
+ .split(',')
126
+ .map((s) => s.trim())
127
+ .filter(Boolean);
128
+ const init = await initialize(endpoint);
129
+ const out = await callTool({
130
+ endpoint,
131
+ sessionId: init.sessionId,
132
+ token: init.token,
133
+ apiKey: config.apiKey,
134
+ name: 'edit_transcript',
135
+ args: { transcript_id: transcriptId, deleted_ids: deletedIds },
136
+ });
137
+ const text = extractToolText(out.payload);
138
+ printResult('edit_transcript', text ? JSON.parse(text) : out.payload);
139
+ return;
140
+ }
141
+
142
+ if (cmd === 'render') {
143
+ const transcriptId = flags['transcript-id'];
144
+ if (!transcriptId || typeof transcriptId !== 'string') throw new Error('--transcript-id is required');
145
+ const init = await initialize(endpoint);
146
+ const out = await callTool({
147
+ endpoint,
148
+ sessionId: init.sessionId,
149
+ token: init.token,
150
+ apiKey: config.apiKey,
151
+ name: 'render_audio',
152
+ args: { transcript_id: transcriptId },
153
+ });
154
+ const text = extractToolText(out.payload);
155
+ printResult('render_audio', text ? JSON.parse(text) : out.payload);
156
+ return;
157
+ }
158
+
159
+ throw new Error(`Unknown command: ${cmd}`);
160
+ }
161
+
162
+ main().catch((err) => {
163
+ console.error(err instanceof Error ? err.message : String(err));
164
+ process.exit(1);
165
+ });
@@ -0,0 +1,90 @@
1
+ const ACCEPT = 'application/json, text/event-stream';
2
+
3
+ function stripTrailingSlash(url) {
4
+ return url.endsWith('/') ? url.slice(0, -1) : url;
5
+ }
6
+
7
+ function headers(base = {}) {
8
+ return {
9
+ Accept: ACCEPT,
10
+ 'Content-Type': 'application/json',
11
+ ...base,
12
+ };
13
+ }
14
+
15
+ export async function initialize(endpoint) {
16
+ const body = {
17
+ jsonrpc: '2.0',
18
+ id: 1,
19
+ method: 'initialize',
20
+ params: {
21
+ protocolVersion: '2025-03-26',
22
+ capabilities: {},
23
+ clientInfo: { name: 'mixmake-cli', version: '0.1.0' },
24
+ },
25
+ };
26
+
27
+ const res = await fetch(stripTrailingSlash(endpoint), {
28
+ method: 'POST',
29
+ headers: headers(),
30
+ body: JSON.stringify(body),
31
+ redirect: 'follow',
32
+ });
33
+
34
+ const requestId = res.headers.get('x-request-id');
35
+ const sessionId = res.headers.get('mcp-session-id') || res.headers.get('Mcp-Session-Id');
36
+ const payload = await safeJson(res);
37
+ const token = payload?.result?._meta?.sessionToken;
38
+
39
+ if (!res.ok) {
40
+ throw new Error(`initialize failed (${res.status})${requestId ? ` request_id=${requestId}` : ''}`);
41
+ }
42
+ if (!sessionId || !token) {
43
+ throw new Error(`initialize missing session details${requestId ? ` request_id=${requestId}` : ''}`);
44
+ }
45
+ return { sessionId, token, requestId, payload };
46
+ }
47
+
48
+ export async function callTool({ endpoint, sessionId, token, apiKey, name, args, id = 2 }) {
49
+ const body = {
50
+ jsonrpc: '2.0',
51
+ id,
52
+ method: 'tools/call',
53
+ params: {
54
+ name,
55
+ arguments: args,
56
+ },
57
+ };
58
+ const extraHeaders = {
59
+ Authorization: `Bearer ${token}`,
60
+ 'Mcp-Session-Id': sessionId,
61
+ };
62
+ if (apiKey) extraHeaders['X-Mixmake-Api-Key'] = apiKey;
63
+
64
+ const res = await fetch(stripTrailingSlash(endpoint), {
65
+ method: 'POST',
66
+ headers: headers(extraHeaders),
67
+ body: JSON.stringify(body),
68
+ redirect: 'follow',
69
+ });
70
+ const requestId = res.headers.get('x-request-id');
71
+ const payload = await safeJson(res);
72
+ if (!res.ok) {
73
+ throw new Error(`tool ${name} failed (${res.status})${requestId ? ` request_id=${requestId}` : ''}`);
74
+ }
75
+ if (payload?.error) {
76
+ const msg = payload.error?.message || 'unknown error';
77
+ throw new Error(`tool ${name} error: ${msg}${requestId ? ` request_id=${requestId}` : ''}`);
78
+ }
79
+ return { payload, requestId };
80
+ }
81
+
82
+ function safeJson(res) {
83
+ return res
84
+ .json()
85
+ .catch(async () => ({ raw: await res.text().catch(() => ''), error: { message: 'non-json response' } }));
86
+ }
87
+
88
+ export function extractToolText(payload) {
89
+ return payload?.result?.content?.find?.((item) => item.type === 'text')?.text;
90
+ }