sogni-gen 1.3.1 → 1.3.3
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 +28 -0
- package/desktop-extension/server/mcp-server.mjs +91 -31
- package/desktop-extension/server/sogni-gen.mjs +29 -2
- package/mcp-server.mjs +91 -41
- package/package.json +1 -1
- package/sogni-gen.mjs +29 -2
package/README.md
CHANGED
|
@@ -41,6 +41,33 @@ Restart Claude Desktop after saving. The same natural-language commands work.
|
|
|
41
41
|
|
|
42
42
|
> **Note:** Both Claude Code and Claude Desktop require Sogni credentials — see [Setup](#setup) below.
|
|
43
43
|
|
|
44
|
+
### Global npm Install (CLI + MCP)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g sogni-gen
|
|
48
|
+
sogni-gen --version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If `sogni-gen-mcp` is on your `PATH`, you can register it directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Claude Code using globally installed binary
|
|
55
|
+
claude mcp add sogni -- sogni-gen-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Claude Desktop config using global binary:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"sogni": {
|
|
64
|
+
"command": "sogni-gen-mcp",
|
|
65
|
+
"args": []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
44
71
|
### Quick Install (OpenClaw) - Recommended
|
|
45
72
|
|
|
46
73
|
Point your OpenClaw to the [`llm.txt`](https://raw.githubusercontent.com/Sogni-AI/openclaw-sogni-gen/main/llm.txt) and everything is set up — just paste the URL into Telegram, WhatsApp, or iMessages and the bot handles image and video generation automatically.
|
|
@@ -269,6 +296,7 @@ Multi-angle mode auto-builds the `<sks>` prompt and applies the `multiple_angles
|
|
|
269
296
|
--lora-strengths <n> Comma-separated LoRA strengths
|
|
270
297
|
--token-type <type> spark|sogni
|
|
271
298
|
--balance, --balances Show SPARK/SOGNI balances and exit
|
|
299
|
+
--version, -V Show sogni-gen version and exit
|
|
272
300
|
--video, -v Generate video instead of image
|
|
273
301
|
--workflow <type> t2v|i2v|s2v|animate-move|animate-replace
|
|
274
302
|
--fps <num> Frames per second (video)
|
|
@@ -18,7 +18,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { dirname, join } from 'path';
|
|
21
|
-
import { existsSync } from 'fs';
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
22
|
import { homedir } from 'os';
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -29,6 +29,14 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
29
29
|
const __dirname = dirname(__filename);
|
|
30
30
|
const SOGNI_GEN = join(__dirname, 'sogni-gen.mjs');
|
|
31
31
|
const CREDENTIALS_PATH = join(homedir(), '.config', 'sogni', 'credentials');
|
|
32
|
+
const SERVER_VERSION = (() => {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
35
|
+
return pkg.version || 'unknown';
|
|
36
|
+
} catch {
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
32
40
|
|
|
33
41
|
// ---------------------------------------------------------------------------
|
|
34
42
|
// CLI spawning helper
|
|
@@ -110,7 +118,7 @@ function checkCredentials() {
|
|
|
110
118
|
// Result formatting
|
|
111
119
|
// ---------------------------------------------------------------------------
|
|
112
120
|
|
|
113
|
-
function formatSuccess(result) {
|
|
121
|
+
async function formatSuccess(result) {
|
|
114
122
|
const parts = [];
|
|
115
123
|
|
|
116
124
|
if (result.type === 'balance') {
|
|
@@ -144,13 +152,55 @@ function formatSuccess(result) {
|
|
|
144
152
|
|
|
145
153
|
const content = [{ type: 'text', text: parts.join('\n') }];
|
|
146
154
|
|
|
147
|
-
//
|
|
155
|
+
// Download images/videos and save locally + embed as base64 for Claude Desktop
|
|
156
|
+
const savedPaths = [];
|
|
148
157
|
for (const url of urls) {
|
|
149
|
-
|
|
150
|
-
|
|
158
|
+
const isImage = /\.(png|jpg|jpeg|webp|gif)(\?|$)/i.test(url);
|
|
159
|
+
const isVideo = /\.(mp4|webm|mov)(\?|$)/i.test(url);
|
|
160
|
+
|
|
161
|
+
if (!isImage && !isVideo) continue;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const resp = await fetch(url);
|
|
165
|
+
if (!resp.ok) continue;
|
|
166
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
167
|
+
|
|
168
|
+
// Determine extension and build a temp file path
|
|
169
|
+
const ext = isImage
|
|
170
|
+
? (url.match(/\.(png|jpg|jpeg|webp|gif)/i)?.[1]?.toLowerCase() || 'png')
|
|
171
|
+
: (url.match(/\.(mp4|webm|mov)/i)?.[1]?.toLowerCase() || 'mp4');
|
|
172
|
+
|
|
173
|
+
// Save to ~/Downloads/sogni/ so the user can find it easily
|
|
174
|
+
const { mkdirSync, writeFileSync } = await import('fs');
|
|
175
|
+
const downloadsDir = join(homedir(), 'Downloads', 'sogni');
|
|
176
|
+
mkdirSync(downloadsDir, { recursive: true });
|
|
177
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
178
|
+
const filename = `sogni-${timestamp}-${savedPaths.length}.${ext}`;
|
|
179
|
+
const filePath = join(downloadsDir, filename);
|
|
180
|
+
writeFileSync(filePath, buf);
|
|
181
|
+
savedPaths.push(filePath);
|
|
182
|
+
|
|
183
|
+
// For images, embed as base64 (Claude Desktop can render these inline)
|
|
184
|
+
if (isImage) {
|
|
185
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg'
|
|
186
|
+
: ext === 'webp' ? 'image/webp'
|
|
187
|
+
: ext === 'gif' ? 'image/gif'
|
|
188
|
+
: 'image/png';
|
|
189
|
+
content.push({ type: 'image', data: buf.toString('base64'), mimeType });
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// If download fails, skip — the URL is still in the text above
|
|
151
193
|
}
|
|
152
194
|
}
|
|
153
195
|
|
|
196
|
+
// Append saved file paths to the text output
|
|
197
|
+
if (savedPaths.length > 0) {
|
|
198
|
+
const textBlock = content[0];
|
|
199
|
+
textBlock.text += '\n\n' + savedPaths.map((p, i) =>
|
|
200
|
+
savedPaths.length === 1 ? `📁 Saved: ${p}` : `📁 Saved #${i + 1}: ${p}`
|
|
201
|
+
).join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
return { content };
|
|
155
205
|
}
|
|
156
206
|
|
|
@@ -161,11 +211,20 @@ function formatError(result) {
|
|
|
161
211
|
return { content: [{ type: 'text', text: parts.join('\n') }], isError: true };
|
|
162
212
|
}
|
|
163
213
|
|
|
164
|
-
function formatResult(result) {
|
|
214
|
+
async function formatResult(result) {
|
|
165
215
|
if (result.success === false) return formatError(result);
|
|
166
216
|
return formatSuccess(result);
|
|
167
217
|
}
|
|
168
218
|
|
|
219
|
+
async function runAndFormat(args, { timeoutMs = 30_000, requireCredentials = true } = {}) {
|
|
220
|
+
if (requireCredentials) {
|
|
221
|
+
const credErr = checkCredentials();
|
|
222
|
+
if (credErr) return credErr;
|
|
223
|
+
}
|
|
224
|
+
const result = await runSogniGen(args, { timeoutMs });
|
|
225
|
+
return formatResult(result);
|
|
226
|
+
}
|
|
227
|
+
|
|
169
228
|
// ---------------------------------------------------------------------------
|
|
170
229
|
// Tool definitions
|
|
171
230
|
// ---------------------------------------------------------------------------
|
|
@@ -446,6 +505,14 @@ The face likeness is preserved while applying the style from the prompt.`,
|
|
|
446
505
|
properties: {},
|
|
447
506
|
},
|
|
448
507
|
},
|
|
508
|
+
{
|
|
509
|
+
name: 'get_version',
|
|
510
|
+
description: 'Show the running sogni-gen version for this MCP server instance.',
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: 'object',
|
|
513
|
+
properties: {},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
449
516
|
];
|
|
450
517
|
|
|
451
518
|
// ---------------------------------------------------------------------------
|
|
@@ -453,9 +520,6 @@ The face likeness is preserved while applying the style from the prompt.`,
|
|
|
453
520
|
// ---------------------------------------------------------------------------
|
|
454
521
|
|
|
455
522
|
async function handleGenerateImage(params) {
|
|
456
|
-
const credErr = checkCredentials();
|
|
457
|
-
if (credErr) return credErr;
|
|
458
|
-
|
|
459
523
|
const args = [params.prompt];
|
|
460
524
|
if (params.model) args.push('-m', params.model);
|
|
461
525
|
if (params.width) args.push('-w', String(params.width));
|
|
@@ -467,14 +531,10 @@ async function handleGenerateImage(params) {
|
|
|
467
531
|
if (params.loras?.length) args.push('--loras', params.loras.join(','));
|
|
468
532
|
if (params.lora_strengths?.length) args.push('--lora-strengths', params.lora_strengths.join(','));
|
|
469
533
|
|
|
470
|
-
|
|
471
|
-
return formatResult(result);
|
|
534
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
472
535
|
}
|
|
473
536
|
|
|
474
537
|
async function handleGenerateVideo(params) {
|
|
475
|
-
const credErr = checkCredentials();
|
|
476
|
-
if (credErr) return credErr;
|
|
477
|
-
|
|
478
538
|
const args = ['--video', params.prompt];
|
|
479
539
|
if (params.workflow) args.push('--workflow', params.workflow);
|
|
480
540
|
if (params.model) args.push('-m', params.model);
|
|
@@ -491,14 +551,10 @@ async function handleGenerateVideo(params) {
|
|
|
491
551
|
if (params.output) args.push('-o', params.output);
|
|
492
552
|
if (params.looping) args.push('--looping');
|
|
493
553
|
|
|
494
|
-
|
|
495
|
-
return formatResult(result);
|
|
554
|
+
return runAndFormat(args, { timeoutMs: 600_000 });
|
|
496
555
|
}
|
|
497
556
|
|
|
498
557
|
async function handleEditImage(params) {
|
|
499
|
-
const credErr = checkCredentials();
|
|
500
|
-
if (credErr) return credErr;
|
|
501
|
-
|
|
502
558
|
const args = [];
|
|
503
559
|
for (const img of params.context_images) {
|
|
504
560
|
args.push('-c', img);
|
|
@@ -509,14 +565,10 @@ async function handleEditImage(params) {
|
|
|
509
565
|
if (params.height) args.push('-h', String(params.height));
|
|
510
566
|
if (params.output) args.push('-o', params.output);
|
|
511
567
|
|
|
512
|
-
|
|
513
|
-
return formatResult(result);
|
|
568
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
514
569
|
}
|
|
515
570
|
|
|
516
571
|
async function handlePhotobooth(params) {
|
|
517
|
-
const credErr = checkCredentials();
|
|
518
|
-
if (credErr) return credErr;
|
|
519
|
-
|
|
520
572
|
const args = ['--photobooth', '--ref', params.reference_face, params.prompt];
|
|
521
573
|
if (params.model) args.push('-m', params.model);
|
|
522
574
|
if (params.cn_strength != null) args.push('--cn-strength', String(params.cn_strength));
|
|
@@ -526,16 +578,22 @@ async function handlePhotobooth(params) {
|
|
|
526
578
|
if (params.count) args.push('-n', String(params.count));
|
|
527
579
|
if (params.output) args.push('-o', params.output);
|
|
528
580
|
|
|
529
|
-
|
|
530
|
-
return formatResult(result);
|
|
581
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
531
582
|
}
|
|
532
583
|
|
|
533
584
|
async function handleCheckBalance() {
|
|
534
|
-
|
|
535
|
-
|
|
585
|
+
return runAndFormat(['--balance'], { timeoutMs: 30_000 });
|
|
586
|
+
}
|
|
536
587
|
|
|
537
|
-
|
|
538
|
-
|
|
588
|
+
async function handleGetVersion() {
|
|
589
|
+
const result = await runSogniGen(['--version'], { timeoutMs: 5_000 });
|
|
590
|
+
if (result.success === false) return formatError(result);
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: `mcp-server version: ${SERVER_VERSION}\nsogni-gen version: ${result.version || 'unknown'}`,
|
|
595
|
+
}],
|
|
596
|
+
};
|
|
539
597
|
}
|
|
540
598
|
|
|
541
599
|
function handleListModels() {
|
|
@@ -560,7 +618,7 @@ Defaults:
|
|
|
560
618
|
// ---------------------------------------------------------------------------
|
|
561
619
|
|
|
562
620
|
const server = new Server(
|
|
563
|
-
{ name: 'sogni', version:
|
|
621
|
+
{ name: 'sogni', version: SERVER_VERSION },
|
|
564
622
|
{ capabilities: { tools: {} } },
|
|
565
623
|
);
|
|
566
624
|
|
|
@@ -582,6 +640,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
582
640
|
return await handleCheckBalance();
|
|
583
641
|
case 'list_models':
|
|
584
642
|
return handleListModels();
|
|
643
|
+
case 'get_version':
|
|
644
|
+
return await handleGetVersion();
|
|
585
645
|
default:
|
|
586
646
|
return {
|
|
587
647
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -19,6 +19,14 @@ const IS_OPENCLAW_INVOCATION = Boolean(process.env.OPENCLAW_PLUGIN_CONFIG);
|
|
|
19
19
|
const RAW_ARGS = process.argv.slice(2);
|
|
20
20
|
const CLI_WANTS_JSON = RAW_ARGS.includes('--json');
|
|
21
21
|
const JSON_ERROR_MODE = CLI_WANTS_JSON || IS_OPENCLAW_INVOCATION;
|
|
22
|
+
const PACKAGE_VERSION = (() => {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
25
|
+
return pkg.version || 'unknown';
|
|
26
|
+
} catch {
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
22
30
|
const VIDEO_WORKFLOW_DEFAULT_MODELS = {
|
|
23
31
|
't2v': 'wan_v2.2-14b-fp8_t2v_lightx2v',
|
|
24
32
|
'i2v': 'wan_v2.2-14b-fp8_i2v_lightx2v',
|
|
@@ -587,6 +595,7 @@ const options = {
|
|
|
587
595
|
autoResizeVideoAssets: null,
|
|
588
596
|
estimateVideoCost: false,
|
|
589
597
|
showBalance: false,
|
|
598
|
+
showVersion: false,
|
|
590
599
|
angles360Video: null,
|
|
591
600
|
refImage: null, // Reference image for video (start frame)
|
|
592
601
|
refImageEnd: null, // End frame for video interpolation
|
|
@@ -811,6 +820,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
811
820
|
options.estimateVideoCost = true;
|
|
812
821
|
} else if (arg === '--balance' || arg === '--balances') {
|
|
813
822
|
options.showBalance = true;
|
|
823
|
+
} else if (arg === '--version' || arg === '-V') {
|
|
824
|
+
options.showVersion = true;
|
|
814
825
|
} else if (arg === '--help') {
|
|
815
826
|
console.log(`
|
|
816
827
|
sogni-gen - Generate images and videos using Sogni AI
|
|
@@ -873,6 +884,7 @@ General:
|
|
|
873
884
|
--guidance <num> Override guidance (model-dependent)
|
|
874
885
|
--token-type <type> Token type: spark|sogni (default: spark)
|
|
875
886
|
--balance, --balances Show SPARK/SOGNI balances and exit
|
|
887
|
+
--version, -V Show sogni-gen version and exit
|
|
876
888
|
--last Show last render info (JSON)
|
|
877
889
|
--json Output JSON with all details
|
|
878
890
|
--strict-size Do not auto-adjust video size to satisfy i2v reference resizing constraints
|
|
@@ -1199,7 +1211,7 @@ if (options.video) {
|
|
|
1199
1211
|
options.model = options.model || openclawConfig?.defaultImageModel || 'z_image_turbo_bf16';
|
|
1200
1212
|
}
|
|
1201
1213
|
|
|
1202
|
-
if (!options.prompt && !options.estimateVideoCost && !options.multiAngle && !options.showBalance) {
|
|
1214
|
+
if (!options.prompt && !options.estimateVideoCost && !options.multiAngle && !options.showBalance && !options.showVersion) {
|
|
1203
1215
|
fatalCliError('No prompt provided. Use --help for usage.', { code: 'INVALID_ARGUMENT' });
|
|
1204
1216
|
}
|
|
1205
1217
|
|
|
@@ -1452,7 +1464,7 @@ if (options.lastSeed) {
|
|
|
1452
1464
|
}
|
|
1453
1465
|
}
|
|
1454
1466
|
|
|
1455
|
-
if (!options.estimateVideoCost && (options.seed === null || options.seed === undefined)) {
|
|
1467
|
+
if (!options.estimateVideoCost && !options.showVersion && (options.seed === null || options.seed === undefined)) {
|
|
1456
1468
|
const strategy = options.seedStrategy || openclawConfig?.seedStrategy || 'prompt-hash';
|
|
1457
1469
|
const normalized = normalizeSeedStrategy(strategy) || 'prompt-hash';
|
|
1458
1470
|
options.seedStrategy = normalized;
|
|
@@ -2163,6 +2175,21 @@ async function main() {
|
|
|
2163
2175
|
let client = null;
|
|
2164
2176
|
|
|
2165
2177
|
try {
|
|
2178
|
+
if (options.showVersion) {
|
|
2179
|
+
if (options.json) {
|
|
2180
|
+
console.log(JSON.stringify({
|
|
2181
|
+
success: true,
|
|
2182
|
+
type: 'version',
|
|
2183
|
+
name: 'sogni-gen',
|
|
2184
|
+
version: PACKAGE_VERSION,
|
|
2185
|
+
timestamp: new Date().toISOString()
|
|
2186
|
+
}));
|
|
2187
|
+
} else {
|
|
2188
|
+
console.log(PACKAGE_VERSION);
|
|
2189
|
+
}
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2166
2193
|
const creds = loadCredentials();
|
|
2167
2194
|
log('Connecting to Sogni...');
|
|
2168
2195
|
client = new SogniClientWrapper({
|
package/mcp-server.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { dirname, join } from 'path';
|
|
21
|
-
import { existsSync } from 'fs';
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
22
|
import { homedir } from 'os';
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -29,6 +29,14 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
29
29
|
const __dirname = dirname(__filename);
|
|
30
30
|
const SOGNI_GEN = join(__dirname, 'sogni-gen.mjs');
|
|
31
31
|
const CREDENTIALS_PATH = join(homedir(), '.config', 'sogni', 'credentials');
|
|
32
|
+
const SERVER_VERSION = (() => {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
35
|
+
return pkg.version || 'unknown';
|
|
36
|
+
} catch {
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
32
40
|
|
|
33
41
|
// ---------------------------------------------------------------------------
|
|
34
42
|
// CLI spawning helper
|
|
@@ -144,26 +152,58 @@ async function formatSuccess(result) {
|
|
|
144
152
|
|
|
145
153
|
const content = [{ type: 'text', text: parts.join('\n') }];
|
|
146
154
|
|
|
147
|
-
// Download images and
|
|
155
|
+
// Download images/videos and save locally + embed as base64 for MCP clients
|
|
156
|
+
// that support inline image rendering (e.g. Claude Desktop).
|
|
157
|
+
// For Claude Code (terminal), the saved file path is the primary way to view results.
|
|
158
|
+
const savedPaths = [];
|
|
148
159
|
for (const url of urls) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
const isImage = /\.(png|jpg|jpeg|webp|gif)(\?|$)/i.test(url);
|
|
161
|
+
const isVideo = /\.(mp4|webm|mov)(\?|$)/i.test(url);
|
|
162
|
+
|
|
163
|
+
if (!isImage && !isVideo) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const resp = await fetch(url);
|
|
167
|
+
if (!resp.ok) continue;
|
|
168
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
169
|
+
|
|
170
|
+
// Determine extension and build a temp file path
|
|
171
|
+
const ext = isImage
|
|
172
|
+
? (url.match(/\.(png|jpg|jpeg|webp|gif)/i)?.[1]?.toLowerCase() || 'png')
|
|
173
|
+
: (url.match(/\.(mp4|webm|mov)/i)?.[1]?.toLowerCase() || 'mp4');
|
|
174
|
+
|
|
175
|
+
// Save to ~/Downloads/sogni/ so the user can find it easily
|
|
176
|
+
const { mkdirSync, writeFileSync } = await import('fs');
|
|
177
|
+
const downloadsDir = join(homedir(), 'Downloads', 'sogni');
|
|
178
|
+
mkdirSync(downloadsDir, { recursive: true });
|
|
179
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
180
|
+
const filename = `sogni-${timestamp}-${savedPaths.length}.${ext}`;
|
|
181
|
+
const filePath = join(downloadsDir, filename);
|
|
182
|
+
writeFileSync(filePath, buf);
|
|
183
|
+
savedPaths.push(filePath);
|
|
184
|
+
|
|
185
|
+
// For images, also embed as base64 (Claude Desktop can render these)
|
|
186
|
+
if (isImage) {
|
|
187
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg'
|
|
188
|
+
: ext === 'webp' ? 'image/webp'
|
|
189
|
+
: ext === 'gif' ? 'image/gif'
|
|
190
|
+
: 'image/png';
|
|
191
|
+
content.push({ type: 'image', data: buf.toString('base64'), mimeType });
|
|
163
192
|
}
|
|
193
|
+
} catch {
|
|
194
|
+
// If download fails, skip — the URL is still in the text above
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
197
|
|
|
198
|
+
// Append saved file paths to the text output so Claude Code users can see/open them
|
|
199
|
+
if (savedPaths.length > 0) {
|
|
200
|
+
const textBlock = content[0];
|
|
201
|
+
textBlock.text += '\n\n' + savedPaths.map((p, i) =>
|
|
202
|
+
savedPaths.length === 1 ? `📁 Saved: ${p}` : `📁 Saved #${i + 1}: ${p}`
|
|
203
|
+
).join('\n');
|
|
204
|
+
textBlock.text += '\n\nTip: In Claude Code, ask Claude to run `open <path>` to view the file.';
|
|
205
|
+
}
|
|
206
|
+
|
|
167
207
|
return { content };
|
|
168
208
|
}
|
|
169
209
|
|
|
@@ -179,6 +219,15 @@ async function formatResult(result) {
|
|
|
179
219
|
return formatSuccess(result);
|
|
180
220
|
}
|
|
181
221
|
|
|
222
|
+
async function runAndFormat(args, { timeoutMs = 30_000, requireCredentials = true } = {}) {
|
|
223
|
+
if (requireCredentials) {
|
|
224
|
+
const credErr = checkCredentials();
|
|
225
|
+
if (credErr) return credErr;
|
|
226
|
+
}
|
|
227
|
+
const result = await runSogniGen(args, { timeoutMs });
|
|
228
|
+
return formatResult(result);
|
|
229
|
+
}
|
|
230
|
+
|
|
182
231
|
// ---------------------------------------------------------------------------
|
|
183
232
|
// Tool definitions
|
|
184
233
|
// ---------------------------------------------------------------------------
|
|
@@ -459,6 +508,14 @@ The face likeness is preserved while applying the style from the prompt.`,
|
|
|
459
508
|
properties: {},
|
|
460
509
|
},
|
|
461
510
|
},
|
|
511
|
+
{
|
|
512
|
+
name: 'get_version',
|
|
513
|
+
description: 'Show the running sogni-gen version for this MCP server instance.',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
462
519
|
];
|
|
463
520
|
|
|
464
521
|
// ---------------------------------------------------------------------------
|
|
@@ -466,9 +523,6 @@ The face likeness is preserved while applying the style from the prompt.`,
|
|
|
466
523
|
// ---------------------------------------------------------------------------
|
|
467
524
|
|
|
468
525
|
async function handleGenerateImage(params) {
|
|
469
|
-
const credErr = checkCredentials();
|
|
470
|
-
if (credErr) return credErr;
|
|
471
|
-
|
|
472
526
|
const args = [params.prompt];
|
|
473
527
|
if (params.model) args.push('-m', params.model);
|
|
474
528
|
if (params.width) args.push('-w', String(params.width));
|
|
@@ -480,14 +534,10 @@ async function handleGenerateImage(params) {
|
|
|
480
534
|
if (params.loras?.length) args.push('--loras', params.loras.join(','));
|
|
481
535
|
if (params.lora_strengths?.length) args.push('--lora-strengths', params.lora_strengths.join(','));
|
|
482
536
|
|
|
483
|
-
|
|
484
|
-
return formatResult(result);
|
|
537
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
485
538
|
}
|
|
486
539
|
|
|
487
540
|
async function handleGenerateVideo(params) {
|
|
488
|
-
const credErr = checkCredentials();
|
|
489
|
-
if (credErr) return credErr;
|
|
490
|
-
|
|
491
541
|
const args = ['--video', params.prompt];
|
|
492
542
|
if (params.workflow) args.push('--workflow', params.workflow);
|
|
493
543
|
if (params.model) args.push('-m', params.model);
|
|
@@ -504,14 +554,10 @@ async function handleGenerateVideo(params) {
|
|
|
504
554
|
if (params.output) args.push('-o', params.output);
|
|
505
555
|
if (params.looping) args.push('--looping');
|
|
506
556
|
|
|
507
|
-
|
|
508
|
-
return formatResult(result);
|
|
557
|
+
return runAndFormat(args, { timeoutMs: 600_000 });
|
|
509
558
|
}
|
|
510
559
|
|
|
511
560
|
async function handleEditImage(params) {
|
|
512
|
-
const credErr = checkCredentials();
|
|
513
|
-
if (credErr) return credErr;
|
|
514
|
-
|
|
515
561
|
const args = [];
|
|
516
562
|
for (const img of params.context_images) {
|
|
517
563
|
args.push('-c', img);
|
|
@@ -522,14 +568,10 @@ async function handleEditImage(params) {
|
|
|
522
568
|
if (params.height) args.push('-h', String(params.height));
|
|
523
569
|
if (params.output) args.push('-o', params.output);
|
|
524
570
|
|
|
525
|
-
|
|
526
|
-
return formatResult(result);
|
|
571
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
527
572
|
}
|
|
528
573
|
|
|
529
574
|
async function handlePhotobooth(params) {
|
|
530
|
-
const credErr = checkCredentials();
|
|
531
|
-
if (credErr) return credErr;
|
|
532
|
-
|
|
533
575
|
const args = ['--photobooth', '--ref', params.reference_face, params.prompt];
|
|
534
576
|
if (params.model) args.push('-m', params.model);
|
|
535
577
|
if (params.cn_strength != null) args.push('--cn-strength', String(params.cn_strength));
|
|
@@ -539,16 +581,22 @@ async function handlePhotobooth(params) {
|
|
|
539
581
|
if (params.count) args.push('-n', String(params.count));
|
|
540
582
|
if (params.output) args.push('-o', params.output);
|
|
541
583
|
|
|
542
|
-
|
|
543
|
-
return formatResult(result);
|
|
584
|
+
return runAndFormat(args, { timeoutMs: 60_000 });
|
|
544
585
|
}
|
|
545
586
|
|
|
546
587
|
async function handleCheckBalance() {
|
|
547
|
-
|
|
548
|
-
|
|
588
|
+
return runAndFormat(['--balance'], { timeoutMs: 30_000 });
|
|
589
|
+
}
|
|
549
590
|
|
|
550
|
-
|
|
551
|
-
|
|
591
|
+
async function handleGetVersion() {
|
|
592
|
+
const result = await runSogniGen(['--version'], { timeoutMs: 5_000 });
|
|
593
|
+
if (result.success === false) return formatError(result);
|
|
594
|
+
return {
|
|
595
|
+
content: [{
|
|
596
|
+
type: 'text',
|
|
597
|
+
text: `mcp-server version: ${SERVER_VERSION}\nsogni-gen version: ${result.version || 'unknown'}`,
|
|
598
|
+
}],
|
|
599
|
+
};
|
|
552
600
|
}
|
|
553
601
|
|
|
554
602
|
function handleListModels() {
|
|
@@ -573,7 +621,7 @@ Defaults:
|
|
|
573
621
|
// ---------------------------------------------------------------------------
|
|
574
622
|
|
|
575
623
|
const server = new Server(
|
|
576
|
-
{ name: 'sogni', version:
|
|
624
|
+
{ name: 'sogni', version: SERVER_VERSION },
|
|
577
625
|
{ capabilities: { tools: {} } },
|
|
578
626
|
);
|
|
579
627
|
|
|
@@ -595,6 +643,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
595
643
|
return await handleCheckBalance();
|
|
596
644
|
case 'list_models':
|
|
597
645
|
return handleListModels();
|
|
646
|
+
case 'get_version':
|
|
647
|
+
return await handleGetVersion();
|
|
598
648
|
default:
|
|
599
649
|
return {
|
|
600
650
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
package/package.json
CHANGED
package/sogni-gen.mjs
CHANGED
|
@@ -19,6 +19,14 @@ const IS_OPENCLAW_INVOCATION = Boolean(process.env.OPENCLAW_PLUGIN_CONFIG);
|
|
|
19
19
|
const RAW_ARGS = process.argv.slice(2);
|
|
20
20
|
const CLI_WANTS_JSON = RAW_ARGS.includes('--json');
|
|
21
21
|
const JSON_ERROR_MODE = CLI_WANTS_JSON || IS_OPENCLAW_INVOCATION;
|
|
22
|
+
const PACKAGE_VERSION = (() => {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
25
|
+
return pkg.version || 'unknown';
|
|
26
|
+
} catch {
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
22
30
|
const VIDEO_WORKFLOW_DEFAULT_MODELS = {
|
|
23
31
|
't2v': 'wan_v2.2-14b-fp8_t2v_lightx2v',
|
|
24
32
|
'i2v': 'wan_v2.2-14b-fp8_i2v_lightx2v',
|
|
@@ -587,6 +595,7 @@ const options = {
|
|
|
587
595
|
autoResizeVideoAssets: null,
|
|
588
596
|
estimateVideoCost: false,
|
|
589
597
|
showBalance: false,
|
|
598
|
+
showVersion: false,
|
|
590
599
|
angles360Video: null,
|
|
591
600
|
refImage: null, // Reference image for video (start frame)
|
|
592
601
|
refImageEnd: null, // End frame for video interpolation
|
|
@@ -811,6 +820,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
811
820
|
options.estimateVideoCost = true;
|
|
812
821
|
} else if (arg === '--balance' || arg === '--balances') {
|
|
813
822
|
options.showBalance = true;
|
|
823
|
+
} else if (arg === '--version' || arg === '-V') {
|
|
824
|
+
options.showVersion = true;
|
|
814
825
|
} else if (arg === '--help') {
|
|
815
826
|
console.log(`
|
|
816
827
|
sogni-gen - Generate images and videos using Sogni AI
|
|
@@ -873,6 +884,7 @@ General:
|
|
|
873
884
|
--guidance <num> Override guidance (model-dependent)
|
|
874
885
|
--token-type <type> Token type: spark|sogni (default: spark)
|
|
875
886
|
--balance, --balances Show SPARK/SOGNI balances and exit
|
|
887
|
+
--version, -V Show sogni-gen version and exit
|
|
876
888
|
--last Show last render info (JSON)
|
|
877
889
|
--json Output JSON with all details
|
|
878
890
|
--strict-size Do not auto-adjust video size to satisfy i2v reference resizing constraints
|
|
@@ -1199,7 +1211,7 @@ if (options.video) {
|
|
|
1199
1211
|
options.model = options.model || openclawConfig?.defaultImageModel || 'z_image_turbo_bf16';
|
|
1200
1212
|
}
|
|
1201
1213
|
|
|
1202
|
-
if (!options.prompt && !options.estimateVideoCost && !options.multiAngle && !options.showBalance) {
|
|
1214
|
+
if (!options.prompt && !options.estimateVideoCost && !options.multiAngle && !options.showBalance && !options.showVersion) {
|
|
1203
1215
|
fatalCliError('No prompt provided. Use --help for usage.', { code: 'INVALID_ARGUMENT' });
|
|
1204
1216
|
}
|
|
1205
1217
|
|
|
@@ -1452,7 +1464,7 @@ if (options.lastSeed) {
|
|
|
1452
1464
|
}
|
|
1453
1465
|
}
|
|
1454
1466
|
|
|
1455
|
-
if (!options.estimateVideoCost && (options.seed === null || options.seed === undefined)) {
|
|
1467
|
+
if (!options.estimateVideoCost && !options.showVersion && (options.seed === null || options.seed === undefined)) {
|
|
1456
1468
|
const strategy = options.seedStrategy || openclawConfig?.seedStrategy || 'prompt-hash';
|
|
1457
1469
|
const normalized = normalizeSeedStrategy(strategy) || 'prompt-hash';
|
|
1458
1470
|
options.seedStrategy = normalized;
|
|
@@ -2163,6 +2175,21 @@ async function main() {
|
|
|
2163
2175
|
let client = null;
|
|
2164
2176
|
|
|
2165
2177
|
try {
|
|
2178
|
+
if (options.showVersion) {
|
|
2179
|
+
if (options.json) {
|
|
2180
|
+
console.log(JSON.stringify({
|
|
2181
|
+
success: true,
|
|
2182
|
+
type: 'version',
|
|
2183
|
+
name: 'sogni-gen',
|
|
2184
|
+
version: PACKAGE_VERSION,
|
|
2185
|
+
timestamp: new Date().toISOString()
|
|
2186
|
+
}));
|
|
2187
|
+
} else {
|
|
2188
|
+
console.log(PACKAGE_VERSION);
|
|
2189
|
+
}
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2166
2193
|
const creds = loadCredentials();
|
|
2167
2194
|
log('Connecting to Sogni...');
|
|
2168
2195
|
client = new SogniClientWrapper({
|