@mindstudio-ai/local-model-tunnel 0.3.3 → 0.4.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.
@@ -10,6 +10,7 @@ var config = new Conf({
10
10
  environment: "prod",
11
11
  providerBaseUrls: {},
12
12
  providerInstallPaths: {},
13
+ localInterfaces: {},
13
14
  environments: {
14
15
  prod: {
15
16
  apiBaseUrl: "https://api.mindstudio.ai"
@@ -37,6 +38,12 @@ function getApiKey() {
37
38
  function setApiKey(key) {
38
39
  setEnvConfig("apiKey", key);
39
40
  }
41
+ function getUserId() {
42
+ return getEnvConfig().userId;
43
+ }
44
+ function setUserId(id) {
45
+ setEnvConfig("userId", id);
46
+ }
40
47
  function getApiBaseUrl() {
41
48
  return getEnvConfig().apiBaseUrl;
42
49
  }
@@ -61,6 +68,23 @@ function setProviderInstallPath(name, installPath) {
61
68
  paths[name] = installPath;
62
69
  config.set("providerInstallPaths", paths);
63
70
  }
71
+ function getLocalInterfacesDir() {
72
+ return path.join(os.homedir(), ".mindstudio-local-tunnel", "interfaces");
73
+ }
74
+ function getLocalInterfacePath(key) {
75
+ const interfaces = config.get("localInterfaces");
76
+ return interfaces[key];
77
+ }
78
+ function setLocalInterfacePath(key, dirPath) {
79
+ const interfaces = config.get("localInterfaces");
80
+ interfaces[key] = dirPath;
81
+ config.set("localInterfaces", interfaces);
82
+ }
83
+ function deleteLocalInterfacePath(key) {
84
+ const interfaces = config.get("localInterfaces");
85
+ delete interfaces[key];
86
+ config.set("localInterfaces", interfaces);
87
+ }
64
88
 
65
89
  // src/api.ts
66
90
  function getHeaders() {
@@ -68,16 +92,21 @@ function getHeaders() {
68
92
  if (!apiKey) {
69
93
  throw new Error("Not authenticated. Run: mindstudio-local auth");
70
94
  }
71
- return {
95
+ const headers = {
72
96
  Authorization: `Bearer ${apiKey}`,
73
97
  "Content-Type": "application/json"
74
98
  };
99
+ const userId = getUserId();
100
+ if (userId) {
101
+ headers["x-user-id"] = userId;
102
+ }
103
+ return headers;
75
104
  }
76
- async function pollForRequest(models) {
105
+ async function pollForRequest(modelIds) {
77
106
  const baseUrl = getApiBaseUrl();
78
- const modelsParam = models.join(",");
107
+ const modelIdsParam = modelIds.join(",");
79
108
  const response = await fetch(
80
- `${baseUrl}/v1/local-models/poll?models=${encodeURIComponent(modelsParam)}`,
109
+ `${baseUrl}/v1/local-models/poll?modelIds=${encodeURIComponent(modelIdsParam)}`,
81
110
  {
82
111
  method: "GET",
83
112
  headers: getHeaders()
@@ -136,52 +165,18 @@ async function verifyApiKey() {
136
165
  return false;
137
166
  }
138
167
  }
139
- async function syncLocalModel(modelNameOrOptions, provider = "ollama", modelType = "llm_chat") {
168
+ async function syncModels(models) {
140
169
  const baseUrl = getApiBaseUrl();
141
- let payload;
142
- if (typeof modelNameOrOptions === "string") {
143
- payload = {
144
- modelName: modelNameOrOptions,
145
- provider,
146
- modelType
147
- };
148
- } else {
149
- payload = {
150
- modelName: modelNameOrOptions.modelName,
151
- provider: modelNameOrOptions.provider,
152
- modelType: modelNameOrOptions.modelType || "llm_chat",
153
- parameters: modelNameOrOptions.parameters
154
- };
155
- }
156
- const response = await fetch(`${baseUrl}/v1/local-models/models/create`, {
170
+ const response = await fetch(`${baseUrl}/v1/local-models/models/sync`, {
157
171
  method: "POST",
158
172
  headers: getHeaders(),
159
- body: JSON.stringify(payload)
173
+ body: JSON.stringify({ models })
160
174
  });
161
175
  if (!response.ok) {
162
176
  const errorText = await response.text();
163
177
  throw new Error(`Sync failed: ${response.status} ${errorText}`);
164
178
  }
165
179
  }
166
- async function updateLocalModel(options) {
167
- const baseUrl = getApiBaseUrl();
168
- const payload = {
169
- modelId: options.modelId,
170
- modelName: options.modelName,
171
- provider: options.provider,
172
- modelType: options.modelType || "llm_chat",
173
- parameters: options.parameters
174
- };
175
- const response = await fetch(`${baseUrl}/v1/local-models/models/update`, {
176
- method: "POST",
177
- headers: getHeaders(),
178
- body: JSON.stringify(payload)
179
- });
180
- if (!response.ok) {
181
- const errorText = await response.text();
182
- throw new Error(`Update failed: ${response.status} ${errorText}`);
183
- }
184
- }
185
180
  async function getSyncedModels() {
186
181
  const baseUrl = getApiBaseUrl();
187
182
  const response = await fetch(`${baseUrl}/v1/local-models/models`, {
@@ -224,6 +219,21 @@ async function pollDeviceAuth(token) {
224
219
  const data = await response.json();
225
220
  return data;
226
221
  }
222
+ async function getEditorSessions() {
223
+ const baseUrl = getApiBaseUrl();
224
+ const response = await fetch(`${baseUrl}/v1/local-editor/sessions`, {
225
+ method: "GET",
226
+ headers: getHeaders()
227
+ });
228
+ if (!response.ok) {
229
+ const errorText = await response.text();
230
+ throw new Error(
231
+ `Failed to fetch editor sessions: ${response.status} ${errorText}`
232
+ );
233
+ }
234
+ const data = await response.json();
235
+ return data.editors;
236
+ }
227
237
  async function disconnectHeartbeat() {
228
238
  const baseUrl = getApiBaseUrl();
229
239
  const response = await fetch(`${baseUrl}/v1/local-models/disconnect`, {
@@ -1325,14 +1335,6 @@ async function discoverAllModels() {
1325
1335
  );
1326
1336
  return modelArrays.flat();
1327
1337
  }
1328
- async function getProviderStatuses() {
1329
- return Promise.all(
1330
- allProviders.map(async (provider) => ({
1331
- provider,
1332
- running: await provider.isRunning()
1333
- }))
1334
- );
1335
- }
1336
1338
  async function detectAllProviderStatuses() {
1337
1339
  return Promise.all(
1338
1340
  allProviders.map(async (provider) => ({
@@ -1390,18 +1392,18 @@ var requestEvents = new RequestEventEmitter();
1390
1392
  // src/runner.ts
1391
1393
  var TunnelRunner = class {
1392
1394
  isRunning = false;
1393
- modelProviderMap = /* @__PURE__ */ new Map();
1394
- models = [];
1395
+ modelMap = /* @__PURE__ */ new Map();
1396
+ modelIds = [];
1395
1397
  /**
1396
- * Start with a pre-discovered list of model names.
1398
+ * Start with a pre-discovered list of synced models.
1397
1399
  * Used by the TUI, which discovers models itself.
1398
1400
  */
1399
- async start(models) {
1401
+ async start(syncedModels) {
1400
1402
  if (this.isRunning) return;
1401
- this.models = models;
1403
+ this.modelIds = syncedModels.map((m) => m.id);
1402
1404
  this.isRunning = true;
1403
1405
  const allModels = await discoverAllModels();
1404
- this.buildModelProviderMap(allModels);
1406
+ this.buildModelMap(syncedModels, allModels);
1405
1407
  this.pollLoop();
1406
1408
  }
1407
1409
  stop() {
@@ -1409,19 +1411,29 @@ var TunnelRunner = class {
1409
1411
  disconnectHeartbeat().catch(() => {
1410
1412
  });
1411
1413
  }
1412
- buildModelProviderMap(models) {
1413
- this.modelProviderMap.clear();
1414
- for (const model of models) {
1415
- const provider = getProvider(model.provider);
1416
- if (provider) {
1417
- this.modelProviderMap.set(model.name, provider);
1414
+ buildModelMap(syncedModels, localModels) {
1415
+ this.modelMap.clear();
1416
+ const localByName = /* @__PURE__ */ new Map();
1417
+ for (const model of localModels) {
1418
+ localByName.set(model.name, model);
1419
+ }
1420
+ for (const synced of syncedModels) {
1421
+ const local = localByName.get(synced.name);
1422
+ if (local) {
1423
+ const provider = getProvider(local.provider);
1424
+ if (provider) {
1425
+ this.modelMap.set(synced.id, {
1426
+ provider,
1427
+ localModelName: local.name
1428
+ });
1429
+ }
1418
1430
  }
1419
1431
  }
1420
1432
  }
1421
1433
  async pollLoop() {
1422
1434
  while (this.isRunning) {
1423
1435
  try {
1424
- const request = await pollForRequest(this.models);
1436
+ const request = await pollForRequest(this.modelIds);
1425
1437
  if (request) {
1426
1438
  this.processRequest(request);
1427
1439
  }
@@ -1438,8 +1450,8 @@ var TunnelRunner = class {
1438
1450
  requestType: request.requestType,
1439
1451
  timestamp: startTime
1440
1452
  });
1441
- const provider = this.modelProviderMap.get(request.modelId);
1442
- if (!provider) {
1453
+ const mapping = this.modelMap.get(request.modelId);
1454
+ if (!mapping) {
1443
1455
  const error = `Model ${request.modelId} not found`;
1444
1456
  await submitResult(request.id, false, void 0, error);
1445
1457
  requestEvents.emitComplete({
@@ -1453,13 +1465,13 @@ var TunnelRunner = class {
1453
1465
  try {
1454
1466
  switch (request.requestType) {
1455
1467
  case "llm_chat":
1456
- await this.handleTextRequest(request, provider, startTime);
1468
+ await this.handleTextRequest(request, mapping, startTime);
1457
1469
  break;
1458
1470
  case "image_generation":
1459
- await this.handleImageRequest(request, provider, startTime);
1471
+ await this.handleImageRequest(request, mapping, startTime);
1460
1472
  break;
1461
1473
  case "video_generation":
1462
- await this.handleVideoRequest(request, provider, startTime);
1474
+ await this.handleVideoRequest(request, mapping, startTime);
1463
1475
  break;
1464
1476
  default:
1465
1477
  throw new Error(`Unsupported request type: ${request.requestType}`);
@@ -1475,7 +1487,7 @@ var TunnelRunner = class {
1475
1487
  });
1476
1488
  }
1477
1489
  }
1478
- async handleTextRequest(request, provider, startTime) {
1490
+ async handleTextRequest(request, { provider, localModelName }, startTime) {
1479
1491
  if (!provider.chat) {
1480
1492
  throw new Error(`Provider does not support text generation`);
1481
1493
  }
@@ -1483,7 +1495,7 @@ var TunnelRunner = class {
1483
1495
  role: m.role,
1484
1496
  content: m.content
1485
1497
  }));
1486
- const stream = provider.chat(request.modelId, messages, {
1498
+ const stream = provider.chat(localModelName, messages, {
1487
1499
  temperature: request.payload.temperature,
1488
1500
  maxTokens: request.payload.maxTokens
1489
1501
  });
@@ -1514,14 +1526,14 @@ var TunnelRunner = class {
1514
1526
  result: { chars: fullContent.length }
1515
1527
  });
1516
1528
  }
1517
- async handleImageRequest(request, provider, startTime) {
1529
+ async handleImageRequest(request, { provider, localModelName }, startTime) {
1518
1530
  if (!provider.generateImage) {
1519
1531
  throw new Error(`Provider does not support image generation`);
1520
1532
  }
1521
1533
  const prompt = request.payload.prompt || "";
1522
1534
  const config2 = request.payload.config || {};
1523
1535
  const result = await provider.generateImage(
1524
- request.modelId,
1536
+ localModelName,
1525
1537
  prompt,
1526
1538
  {
1527
1539
  negativePrompt: config2.negativePrompt,
@@ -1559,14 +1571,14 @@ var TunnelRunner = class {
1559
1571
  result: { imageSize }
1560
1572
  });
1561
1573
  }
1562
- async handleVideoRequest(request, provider, startTime) {
1574
+ async handleVideoRequest(request, { provider, localModelName }, startTime) {
1563
1575
  if (!provider.generateVideo) {
1564
1576
  throw new Error(`Provider does not support video generation`);
1565
1577
  }
1566
1578
  const prompt = request.payload.prompt || "";
1567
1579
  const config2 = request.payload.config || {};
1568
1580
  const result = await provider.generateVideo(
1569
- request.modelId,
1581
+ localModelName,
1570
1582
  prompt,
1571
1583
  {
1572
1584
  negativePrompt: config2.negativePrompt,
@@ -1616,18 +1628,23 @@ export {
1616
1628
  getEnvironment,
1617
1629
  getApiKey,
1618
1630
  setApiKey,
1631
+ getUserId,
1632
+ setUserId,
1633
+ getApiBaseUrl,
1619
1634
  getConfigPath,
1635
+ getLocalInterfacesDir,
1636
+ getLocalInterfacePath,
1637
+ setLocalInterfacePath,
1638
+ deleteLocalInterfacePath,
1620
1639
  verifyApiKey,
1621
- syncLocalModel,
1622
- updateLocalModel,
1640
+ syncModels,
1623
1641
  getSyncedModels,
1624
1642
  requestDeviceAuth,
1625
1643
  pollDeviceAuth,
1626
- discoverAllModels,
1627
- getProviderStatuses,
1644
+ getEditorSessions,
1628
1645
  detectAllProviderStatuses,
1629
1646
  discoverAllModelsWithParameters,
1630
1647
  requestEvents,
1631
1648
  TunnelRunner
1632
1649
  };
1633
- //# sourceMappingURL=chunk-V3RKCMCQ.js.map
1650
+ //# sourceMappingURL=chunk-44NXQXRB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/api.ts","../src/providers/ollama/index.ts","../src/providers/utils.ts","../src/providers/ollama/readme.md","../src/providers/lmstudio/index.ts","../src/providers/lmstudio/readme.md","../src/providers/stable-diffusion/index.ts","../src/providers/stable-diffusion/readme.md","../src/providers/comfyui/index.ts","../src/providers/comfyui/workflow-discovery.ts","../src/providers/comfyui/converter-install.ts","../src/providers/comfyui/workflow-executor.ts","../src/providers/comfyui/readme.md","../src/providers/index.ts","../src/events.ts","../src/runner.ts"],"sourcesContent":["import Conf from 'conf';\nimport os from 'node:os';\nimport path from 'node:path';\n\nexport type Environment = 'prod' | 'local';\n\ninterface EnvironmentConfig {\n apiKey?: string;\n userId?: string;\n apiBaseUrl: string;\n}\n\ninterface ConfigSchema {\n environment: Environment;\n providerBaseUrls: Record<string, string>;\n providerInstallPaths: Record<string, string>;\n localInterfaces: Record<string, string>;\n environments: {\n prod: EnvironmentConfig;\n local: EnvironmentConfig;\n };\n}\n\nexport const config = new Conf<ConfigSchema>({\n projectName: 'mindstudio-local',\n cwd: path.join(os.homedir(), '.mindstudio-local-tunnel'),\n configName: 'config',\n defaults: {\n environment: 'prod',\n providerBaseUrls: {},\n providerInstallPaths: {},\n localInterfaces: {},\n environments: {\n prod: {\n apiBaseUrl: 'https://api.mindstudio.ai',\n },\n local: {\n apiBaseUrl: 'http://localhost:3129',\n },\n },\n },\n});\n\n// Environment management\nexport function getEnvironment(): Environment {\n return config.get('environment');\n}\n\nexport function setEnvironment(env: Environment): void {\n config.set('environment', env);\n}\n\n// Get config for current environment\nfunction getEnvConfig(): EnvironmentConfig {\n const env = getEnvironment();\n return config.get(`environments.${env}`) as EnvironmentConfig;\n}\n\nfunction setEnvConfig(key: keyof EnvironmentConfig, value: string): void {\n const env = getEnvironment();\n config.set(`environments.${env}.${key}`, value);\n}\n\n// API Key (per environment)\nexport function getApiKey(): string | undefined {\n return getEnvConfig().apiKey;\n}\n\nexport function setApiKey(key: string): void {\n setEnvConfig('apiKey', key);\n}\n\nexport function clearApiKey(): void {\n const env = getEnvironment();\n config.delete(`environments.${env}.apiKey` as keyof ConfigSchema);\n}\n\n// User ID (per environment)\nexport function getUserId(): string | undefined {\n return getEnvConfig().userId;\n}\n\nexport function setUserId(id: string): void {\n setEnvConfig('userId', id);\n}\n\nexport function clearUserId(): void {\n const env = getEnvironment();\n config.delete(`environments.${env}.userId` as keyof ConfigSchema);\n}\n\n// API Base URL (per environment)\nexport function getApiBaseUrl(): string {\n return getEnvConfig().apiBaseUrl;\n}\n\nexport function setApiBaseUrl(url: string): void {\n setEnvConfig('apiBaseUrl', url);\n}\n\nexport function getConfigPath(): string {\n return config.path;\n}\n\n// Provider helpers\nexport function getProviderBaseUrl(name: string, defaultUrl: string): string {\n const urls = config.get('providerBaseUrls');\n return urls[name] ?? defaultUrl;\n}\n\nexport function setProviderBaseUrl(name: string, url: string): void {\n const urls = config.get('providerBaseUrls');\n urls[name] = url;\n config.set('providerBaseUrls', urls);\n}\n\nexport function getProviderInstallPath(name: string): string | undefined {\n const paths = config.get('providerInstallPaths');\n return paths[name];\n}\n\nexport function setProviderInstallPath(\n name: string,\n installPath: string,\n): void {\n const paths = config.get('providerInstallPaths');\n paths[name] = installPath;\n config.set('providerInstallPaths', paths);\n}\n\n// Local interface helpers\nexport function getLocalInterfacesDir(): string {\n return path.join(os.homedir(), '.mindstudio-local-tunnel', 'interfaces');\n}\n\nexport function getLocalInterfacePath(key: string): string | undefined {\n const interfaces = config.get('localInterfaces');\n return interfaces[key];\n}\n\nexport function setLocalInterfacePath(key: string, dirPath: string): void {\n const interfaces = config.get('localInterfaces');\n interfaces[key] = dirPath;\n config.set('localInterfaces', interfaces);\n}\n\nexport function deleteLocalInterfacePath(key: string): void {\n const interfaces = config.get('localInterfaces');\n delete interfaces[key];\n config.set('localInterfaces', interfaces);\n}\n\n// Get all environment info for display\nexport function getEnvironmentInfo(): {\n current: Environment;\n apiBaseUrl: string;\n hasApiKey: boolean;\n} {\n const env = getEnvironment();\n const envConfig = getEnvConfig();\n return {\n current: env,\n apiBaseUrl: envConfig.apiBaseUrl,\n hasApiKey: !!envConfig.apiKey,\n };\n}\n","import { getApiKey, getApiBaseUrl, getUserId } from './config';\n\nexport interface LocalModelRequest {\n id: string;\n organizationId: string;\n modelId: string;\n requestType: 'llm_chat' | 'image_generation' | 'video_generation';\n payload: {\n messages?: Array<{ role: string; content: string }>;\n prompt?: string;\n temperature?: number;\n maxTokens?: number;\n config?: Record<string, unknown>;\n };\n createdAt: number;\n}\n\nfunction getHeaders(): Record<string, string> {\n const apiKey = getApiKey();\n if (!apiKey) {\n throw new Error('Not authenticated. Run: mindstudio-local auth');\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n };\n\n const userId = getUserId();\n if (userId) {\n headers['x-user-id'] = userId;\n }\n\n return headers;\n}\n\nexport async function pollForRequest(\n modelIds: string[],\n): Promise<LocalModelRequest | null> {\n const baseUrl = getApiBaseUrl();\n const modelIdsParam = modelIds.join(',');\n\n const response = await fetch(\n `${baseUrl}/v1/local-models/poll?modelIds=${encodeURIComponent(modelIdsParam)}`,\n {\n method: 'GET',\n headers: getHeaders(),\n },\n );\n\n if (response.status === 204) {\n return null;\n }\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Poll failed: ${response.status} ${error}`);\n }\n const data = (await response.json()) as { request: LocalModelRequest };\n return data.request;\n}\n\n/**\n * Submit a progress update for a running request.\n * @param type - 'chunk' for streaming text content, 'log' for raw log lines\n */\nexport async function submitProgress(\n requestId: string,\n content: string,\n type: 'chunk' | 'log' = 'chunk',\n): Promise<void> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(\n `${baseUrl}/v1/local-models/requests/${requestId}/progress`,\n {\n method: 'POST',\n headers: getHeaders(),\n body: JSON.stringify({ type, content }),\n },\n );\n\n if (!response.ok) {\n console.warn(`Progress update failed: ${response.status}`);\n }\n}\n\n\n/**\n * Result for text/chat completions\n */\nexport interface TextResult {\n content?: string;\n usage?: { promptTokens: number; completionTokens: number };\n}\n\n/**\n * Result for image generation\n */\nexport interface ImageResult {\n /** Base64-encoded image data */\n imageBase64: string;\n /** MIME type (e.g., \"image/png\") */\n mimeType: string;\n /** Seed used for generation */\n seed?: number;\n}\n\n/**\n * Result for video generation\n */\nexport interface VideoResult {\n /** Base64-encoded video data */\n videoBase64: string;\n /** MIME type (e.g., \"video/webp\", \"video/mp4\") */\n mimeType: string;\n /** Duration in seconds */\n duration?: number;\n /** Frames per second */\n fps?: number;\n /** Seed used for generation */\n seed?: number;\n}\n\n/**\n * Combined result type\n */\nexport type RequestResult = TextResult | ImageResult | VideoResult;\n\nexport async function submitResult(\n requestId: string,\n success: boolean,\n result?: RequestResult,\n error?: string,\n): Promise<void> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(\n `${baseUrl}/v1/local-models/requests/${requestId}/result`,\n {\n method: 'POST',\n headers: getHeaders(),\n body: JSON.stringify({ success, result, error }),\n },\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Result submission failed: ${response.status} ${errorText}`,\n );\n }\n}\n\nexport async function verifyApiKey(): Promise<boolean> {\n const baseUrl = getApiBaseUrl();\n\n try {\n const response = await fetch(`${baseUrl}/v1/local-models/verify-api-key`, {\n method: 'GET',\n headers: getHeaders(),\n });\n\n return response.status === 204 || response.ok;\n } catch {\n return false;\n }\n}\n\nexport type ModelTypeMindStudio =\n | 'llm_chat'\n | 'image_generation'\n | 'video_generation';\n\nexport interface SyncModelEntry {\n name: string;\n provider: string;\n type: ModelTypeMindStudio;\n parameters?: unknown[];\n}\n\nexport async function syncModels(\n models: SyncModelEntry[],\n): Promise<void> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/v1/local-models/models/sync`, {\n method: 'POST',\n headers: getHeaders(),\n body: JSON.stringify({ models }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Sync failed: ${response.status} ${errorText}`);\n }\n}\n\nexport interface SyncedModel {\n id: string;\n name: string;\n}\n\nexport async function getSyncedModels(): Promise<SyncedModel[]> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/v1/local-models/models`, {\n method: 'GET',\n headers: getHeaders(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch synced models: ${response.status} ${errorText}`,\n );\n }\n\n const data = (await response.json()) as { models: SyncedModel[] };\n return data.models;\n}\n\nexport async function requestDeviceAuth(): Promise<{\n url: string;\n token: string;\n}> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/developer/v2/request-auth-url`, {\n method: 'GET',\n headers: { 'Content-Type': 'application/json' },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Device auth request failed: ${response.status} ${error}`);\n }\n\n const data = (await response.json()) as {\n url: string;\n token: string;\n };\n\n return data;\n}\n\nexport async function pollDeviceAuth(token: string): Promise<{\n status: 'pending' | 'completed' | 'expired';\n apiKey: string | null;\n userId: string | null;\n}> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/developer/v2/poll-auth-url`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Device auth poll failed: ${response.status} ${error}`);\n }\n\n const data = (await response.json()) as {\n status: 'pending' | 'completed' | 'expired';\n apiKey: string | null;\n userId: string | null;\n };\n\n return data;\n}\n\nexport interface SpaEditorSessionInfo {\n sessionId: string;\n status: string;\n previewDomain: string | null;\n hotUpdateDomain: string | null;\n}\n\nexport interface CustomInterfaceStepInfo {\n stepId: string;\n stepType: string;\n displayName: string;\n workflowId: string;\n workflowName: string;\n spaEditorSession: SpaEditorSessionInfo | null;\n}\n\nexport interface ScriptStepInfo {\n stepId: string;\n displayName: string;\n workflowId: string;\n workflowName: string;\n files: Record<string, string>;\n entryFile: string;\n}\n\nexport interface EditorSession {\n appId: string;\n appName: string;\n customInterfaceSteps: CustomInterfaceStepInfo[];\n scriptSteps: ScriptStepInfo[];\n}\n\nexport async function getEditorSessions(): Promise<EditorSession[]> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/v1/local-editor/sessions`, {\n method: 'GET',\n headers: getHeaders(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch editor sessions: ${response.status} ${errorText}`,\n );\n }\n\n const data = (await response.json()) as { editors: EditorSession[] };\n return data.editors;\n}\n\nexport async function disconnectHeartbeat(): Promise<void> {\n const baseUrl = getApiBaseUrl();\n\n const response = await fetch(`${baseUrl}/v1/local-models/disconnect`, {\n method: 'POST',\n headers: getHeaders(),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Heartbeat disconnect failed: ${response.status} ${error}`);\n }\n}\n","import { Ollama } from 'ollama';\nimport { getProviderBaseUrl } from '../../config';\nimport { commandExists } from '../utils';\nimport readme from './readme.md';\nimport type {\n Provider,\n LocalModel,\n ChatMessage,\n ChatOptions,\n ChatResponse,\n ProviderSetupStatus,\n} from '../types';\n\nclass OllamaProvider implements Provider {\n readonly name = 'ollama';\n readonly displayName = 'Ollama';\n readonly description = 'Run open-source LLMs locally via CLI. Supports Llama, Mistral, Gemma, and more.';\n readonly capabilities = ['text'] as const;\n readonly readme = readme;\n readonly defaultBaseUrl = 'http://localhost:11434';\n\n get baseUrl(): string {\n return getProviderBaseUrl(this.name, this.defaultBaseUrl);\n }\n\n private createClient(): Ollama {\n return new Ollama({ host: this.baseUrl });\n }\n\n async isRunning(): Promise<boolean> {\n try {\n const client = this.createClient();\n await client.list();\n return true;\n } catch {\n return false;\n }\n }\n\n async discoverModels(): Promise<LocalModel[]> {\n try {\n const client = this.createClient();\n const response = await client.list();\n\n return response.models.map((m) => ({\n name: m.name,\n provider: this.name,\n capability: 'text' as const,\n size: m.size,\n parameterSize: m.details?.parameter_size,\n quantization: m.details?.quantization_level,\n }));\n } catch {\n return [];\n }\n }\n\n async detect(): Promise<ProviderSetupStatus> {\n const installed = await commandExists('ollama');\n let running = false;\n\n if (installed) {\n running = await this.isRunning();\n }\n\n return { installed, running };\n }\n\n async *chat(\n model: string,\n messages: ChatMessage[],\n options?: ChatOptions,\n ): AsyncGenerator<ChatResponse> {\n const client = this.createClient();\n\n const stream = await client.chat({\n model,\n messages: messages.map((m) => ({\n role: m.role,\n content: m.content,\n })),\n stream: true,\n options: {\n temperature: options?.temperature,\n num_predict: options?.maxTokens,\n },\n });\n\n for await (const chunk of stream) {\n yield {\n content: chunk.message.content,\n done: chunk.done,\n };\n }\n }\n}\n\nexport default new OllamaProvider();\n","import { exec } from 'child_process';\nimport { promisify } from 'util';\n\nconst execAsync = promisify(exec);\n\n/**\n * Check if a command exists in PATH\n */\nexport async function commandExists(command: string): Promise<boolean> {\n try {\n const checkCmd = process.platform === 'win32' ? 'where' : 'which';\n await execAsync(`${checkCmd} ${command}`);\n return true;\n } catch {\n return false;\n }\n}\n","# Ollama\n\nOllama lets you run text generation models (Llama, Mistral, Gemma, etc.) locally. Once it's running with at least one model downloaded, MindStudio will detect it automatically.\n\n**Default port:** 11434\n**Website:** https://ollama.com\n**GitHub:** https://github.com/ollama/ollama\n\n## Step 1: Install Ollama\n\n### macOS / Linux\n\nOpen a terminal and paste this command:\n\n```\ncurl -fsSL https://ollama.com/install.sh | sh\n```\n\n### macOS (alternative)\n\nDownload the app from https://ollama.com/download, open the file, and drag it into your Applications folder.\n\n### Windows\n\nDownload the installer from https://ollama.com/download and run it. Follow the on-screen instructions.\n\n## Step 2: Start the Server\n\nOpen a terminal and run:\n\n```\nollama serve\n```\n\nLeave this terminal window open -- the server needs to keep running for MindStudio to connect to it.\n\n**macOS tip:** If you installed Ollama as a desktop app, the server starts automatically when you open it. Look for the Ollama icon in your menu bar -- if it's there, you can skip this step.\n\n## Step 3: Download a Model\n\nOpen a **new** terminal window (keep the server running in the other one) and download a model:\n\n```\nollama pull llama3.2\n```\n\nSome good models to start with:\n\n- **llama3.2** -- fast, great all-around model (2 GB download)\n- **mistral** -- efficient for most tasks (4 GB)\n- **gemma2** -- Google's open model (5 GB)\n\nBrowse more models at https://ollama.com/library\n\nOnce the download finishes, go back to the MindStudio tunnel and select **Refresh Providers**. Your models should appear.\n\n## Troubleshooting\n\n- **MindStudio says Ollama is \"not running\"** -- Make sure `ollama serve` is running in a terminal window. You should see \"Listening on 127.0.0.1:11434\" in the output.\n\n- **Ollama is running but no models show up** -- You need to download at least one model first. Run `ollama pull llama3.2` in a separate terminal window.\n\n- **\"address already in use\"** -- Ollama is probably already running. On macOS, check for the Ollama icon in your menu bar. On Linux, run `pkill ollama` and try `ollama serve` again.\n\n- **\"out of memory\" errors** -- Your machine doesn't have enough RAM for that model. Try a smaller one like `llama3.2` (2 GB).\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { getProviderBaseUrl } from '../../config';\nimport readme from './readme.md';\nimport type {\n Provider,\n LocalModel,\n ChatMessage,\n ChatOptions,\n ChatResponse,\n ProviderSetupStatus,\n} from '../types';\n\ninterface LMStudioModel {\n id: string;\n object: string;\n owned_by: string;\n}\n\ninterface LMStudioModelsResponse {\n data: LMStudioModel[];\n}\n\nclass LMStudioProvider implements Provider {\n readonly name = 'lmstudio';\n readonly displayName = 'LM Studio';\n readonly description = 'Desktop app for running LLMs locally with a visual interface. No terminal required.';\n readonly capabilities = ['text'] as const;\n readonly readme = readme;\n readonly defaultBaseUrl = 'http://localhost:1234/v1';\n\n get baseUrl(): string {\n return getProviderBaseUrl(this.name, this.defaultBaseUrl);\n }\n\n private getBaseUrl(): string {\n return this.baseUrl;\n }\n\n async isRunning(): Promise<boolean> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/models`, {\n method: 'GET',\n signal: AbortSignal.timeout(3000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n async discoverModels(): Promise<LocalModel[]> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/models`);\n\n if (!response.ok) {\n return [];\n }\n\n const data = (await response.json()) as LMStudioModelsResponse;\n\n return data.data.map((m) => ({\n name: m.id,\n provider: this.name,\n capability: 'text' as const,\n }));\n } catch {\n return [];\n }\n }\n\n async detect(): Promise<ProviderSetupStatus> {\n let installed = false;\n\n const possiblePaths = {\n darwin: ['/Applications/LM Studio.app'],\n linux: [\n path.join(os.homedir(), '.local/share/LM Studio'),\n '/opt/lm-studio',\n ],\n win32: [\n path.join(process.env.LOCALAPPDATA || '', 'LM Studio'),\n path.join(process.env.PROGRAMFILES || '', 'LM Studio'),\n ],\n };\n\n const paths =\n possiblePaths[process.platform as keyof typeof possiblePaths] || [];\n for (const p of paths) {\n if (fs.existsSync(p)) {\n installed = true;\n break;\n }\n }\n\n let running = false;\n try {\n const response = await fetch('http://localhost:1234/v1/models', {\n signal: AbortSignal.timeout(1000),\n });\n running = response.ok;\n if (running) installed = true;\n } catch {\n running = false;\n }\n\n return { installed, running };\n }\n\n async *chat(\n model: string,\n messages: ChatMessage[],\n options?: ChatOptions,\n ): AsyncGenerator<ChatResponse> {\n const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model,\n messages: messages.map((m) => ({\n role: m.role,\n content: m.content,\n })),\n stream: true,\n temperature: options?.temperature,\n max_tokens: options?.maxTokens,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`LM Studio request failed: ${response.status} ${error}`);\n }\n\n if (!response.body) {\n throw new Error('No response body from LM Studio');\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n yield { content: '', done: true };\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || !trimmed.startsWith('data: ')) {\n continue;\n }\n\n const data = trimmed.slice(6);\n\n if (data === '[DONE]') {\n yield { content: '', done: true };\n return;\n }\n\n try {\n const parsed = JSON.parse(data) as {\n choices: Array<{\n delta?: { content?: string };\n finish_reason?: string | null;\n }>;\n };\n\n const choice = parsed.choices[0];\n const content = choice?.delta?.content || '';\n const isDone = choice?.finish_reason !== null;\n\n if (content) {\n yield { content, done: isDone };\n }\n } catch {\n // Skip malformed JSON chunks\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nexport default new LMStudioProvider();\n","# LM Studio\n\nLM Studio is a desktop app for running text generation models locally. No terminal needed -- everything is done through the app. Once its server is running, MindStudio will detect it automatically.\n\n**Default port:** 1234\n**Website:** https://lmstudio.ai\n**GitHub:** https://github.com/lmstudio-ai\n\n## Step 1: Install LM Studio\n\n1. Go to https://lmstudio.ai\n2. Click the download button for your operating system\n3. Open the downloaded file and install it like any other app\n\n## Step 2: Download a Model\n\n1. Open LM Studio\n2. Click the **Discover** tab on the left sidebar\n3. Search for a model (see suggestions below)\n4. Click the download button next to the model you want\n5. Wait for the download to finish\n\nGood starter models:\n\n- **Llama 3.2** -- great all-around model, fast\n- **Mistral** -- efficient and capable\n- **Phi-3** -- compact, runs well on most machines\n\n## Step 3: Start the Server\n\nThis is the key step -- LM Studio needs to be running its local server for MindStudio to connect.\n\n1. In LM Studio, click the **Developer** tab on the left sidebar\n2. Select a model from the dropdown at the top if one isn't already loaded\n3. Click **Start Server**\n\nYou should see a green indicator showing the server is running on `http://localhost:1234`.\n\n**Important:** Just opening LM Studio is not enough. You must start the server from the Developer tab.\n\nLeave LM Studio open with the server running while you use MindStudio. Go back to the tunnel and select **Refresh Providers** -- your models should appear.\n\n## Troubleshooting\n\n- **MindStudio says LM Studio is \"not running\"** -- Make sure you started the server in the Developer tab. The green indicator should be visible.\n\n- **Server is running but no models show up** -- You need to load a model in the Developer tab. Select one from the dropdown at the top of the Developer tab before starting the server.\n\n- **Port conflict** -- If something else is using port 1234, you can change the port in the Developer tab settings.\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { getProviderBaseUrl, getProviderInstallPath } from '../../config';\nimport readme from './readme.md';\nimport type {\n Provider,\n LocalModel,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImageGenerationProgress,\n ParameterSchema,\n ProviderSetupStatus,\n} from '../types';\n\n/**\n * Response from AUTOMATIC1111's /sdapi/v1/sd-models endpoint\n */\ninterface SDModel {\n title: string;\n model_name: string;\n hash?: string;\n sha256?: string;\n filename: string;\n}\n\n/**\n * Response from AUTOMATIC1111's /sdapi/v1/txt2img endpoint\n */\ninterface Txt2ImgResponse {\n images: string[];\n parameters: Record<string, unknown>;\n info: string;\n}\n\n/**\n * Response from AUTOMATIC1111's /sdapi/v1/progress endpoint\n */\ninterface ProgressResponse {\n progress: number;\n eta_relative: number;\n state: {\n skipped: boolean;\n interrupted: boolean;\n job: string;\n job_count: number;\n job_timestamp: string;\n job_no: number;\n sampling_step: number;\n sampling_steps: number;\n };\n current_image?: string;\n textinfo?: string;\n}\n\n/**\n * Response from AUTOMATIC1111's /sdapi/v1/samplers endpoint\n */\ninterface SDSampler {\n name: string;\n aliases: string[];\n options: Record<string, unknown>;\n}\n\n/**\n * Stable Diffusion provider for AUTOMATIC1111 WebUI\n * Default URL: http://127.0.0.1:7860\n */\nclass StableDiffusionProvider implements Provider {\n readonly name = 'stable-diffusion';\n readonly displayName = 'Stable Diffusion WebUI';\n readonly description = 'Generate images locally using Stable Diffusion checkpoints. Runs as a local web UI.';\n readonly capabilities = ['image'] as const;\n readonly readme = readme;\n readonly defaultBaseUrl = 'http://127.0.0.1:7860';\n\n get baseUrl(): string {\n return getProviderBaseUrl(this.name, this.defaultBaseUrl);\n }\n\n private getBaseUrl(): string {\n return this.baseUrl;\n }\n\n async isRunning(): Promise<boolean> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/sdapi/v1/sd-models`, {\n method: 'GET',\n signal: AbortSignal.timeout(5000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n async discoverModels(): Promise<LocalModel[]> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/sdapi/v1/sd-models`);\n\n if (!response.ok) {\n return [];\n }\n\n const models = (await response.json()) as SDModel[];\n\n return models.map((m) => ({\n name: m.model_name,\n provider: this.name,\n capability: 'image' as const,\n }));\n } catch {\n return [];\n }\n }\n\n async detect(): Promise<ProviderSetupStatus> {\n const savedPath = getProviderInstallPath(this.name);\n\n const possiblePaths = [\n ...(savedPath ? [savedPath] : []),\n path.join(os.homedir(), 'stable-diffusion-webui'),\n path.join(os.homedir(), 'Projects', 'stable-diffusion-webui'),\n path.join(os.homedir(), 'Code', 'stable-diffusion-webui'),\n ];\n\n let installed = false;\n for (const p of possiblePaths) {\n if (\n fs.existsSync(path.join(p, 'launch.py')) ||\n fs.existsSync(path.join(p, 'webui.sh')) ||\n fs.existsSync(path.join(p, 'webui.bat'))\n ) {\n installed = true;\n break;\n }\n }\n\n let running = false;\n try {\n const response = await fetch('http://127.0.0.1:7860/sdapi/v1/sd-models', {\n signal: AbortSignal.timeout(1000),\n });\n running = response.ok;\n if (running) installed = true;\n } catch {\n running = false;\n }\n\n return { installed, running };\n }\n\n /**\n * Get the currently loaded model\n */\n async getCurrentModel(): Promise<string | null> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/sdapi/v1/options`);\n if (!response.ok) return null;\n\n const options = (await response.json()) as {\n sd_model_checkpoint?: string;\n };\n return options.sd_model_checkpoint || null;\n } catch {\n return null;\n }\n }\n\n /**\n * Switch to a different model\n */\n async setModel(modelName: string): Promise<void> {\n const response = await fetch(`${this.getBaseUrl()}/sdapi/v1/options`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sd_model_checkpoint: modelName }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to switch model: ${error}`);\n }\n }\n\n async generateImage(\n model: string,\n prompt: string,\n options?: ImageGenerationOptions,\n onProgress?: (progress: ImageGenerationProgress) => void,\n ): Promise<ImageGenerationResult> {\n const currentModel = await this.getCurrentModel();\n if (currentModel && !currentModel.includes(model)) {\n await this.setModel(model);\n }\n\n const payload = {\n prompt,\n negative_prompt: options?.negativePrompt || '',\n steps: options?.steps || 20,\n width: options?.width || 512,\n height: options?.height || 512,\n cfg_scale: options?.cfgScale || 7,\n seed: options?.seed ?? -1,\n sampler_name: options?.sampler || 'Euler a',\n };\n\n const generatePromise = fetch(`${this.getBaseUrl()}/sdapi/v1/txt2img`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n });\n\n if (onProgress) {\n const pollProgress = async () => {\n while (true) {\n try {\n const response = await fetch(\n `${this.getBaseUrl()}/sdapi/v1/progress`,\n );\n if (!response.ok) break;\n\n const progress = (await response.json()) as ProgressResponse;\n\n onProgress({\n step: progress.state.sampling_step,\n totalSteps: progress.state.sampling_steps,\n preview: progress.current_image,\n });\n\n if (progress.progress >= 1.0) break;\n\n await new Promise((resolve) => setTimeout(resolve, 500));\n } catch {\n break;\n }\n }\n };\n\n pollProgress().catch(() => {});\n }\n\n const response = await generatePromise;\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Image generation failed: ${response.status} ${error}`);\n }\n\n const result = (await response.json()) as Txt2ImgResponse;\n\n if (!result.images || result.images.length === 0) {\n throw new Error('No images returned from Stable Diffusion');\n }\n\n let info: Record<string, unknown> = {};\n let seed: number | undefined;\n try {\n info = JSON.parse(result.info);\n seed = typeof info.seed === 'number' ? info.seed : undefined;\n } catch {\n // Ignore parse errors\n }\n\n return {\n imageBase64: result.images[0],\n mimeType: 'image/png',\n seed,\n info,\n };\n }\n\n /**\n * Fetch available samplers from the backend\n */\n private async getSamplers(): Promise<string[]> {\n try {\n const response = await fetch(`${this.getBaseUrl()}/sdapi/v1/samplers`);\n if (!response.ok) return this.getDefaultSamplers();\n\n const samplers = (await response.json()) as SDSampler[];\n return samplers.map((s) => s.name);\n } catch {\n return this.getDefaultSamplers();\n }\n }\n\n private getDefaultSamplers(): string[] {\n return [\n 'Euler a',\n 'Euler',\n 'LMS',\n 'Heun',\n 'DPM2',\n 'DPM2 a',\n 'DPM++ 2S a',\n 'DPM++ 2M',\n 'DPM++ SDE',\n 'DPM fast',\n 'DPM adaptive',\n 'LMS Karras',\n 'DPM2 Karras',\n 'DPM2 a Karras',\n 'DPM++ 2S a Karras',\n 'DPM++ 2M Karras',\n 'DPM++ SDE Karras',\n 'DDIM',\n 'PLMS',\n 'UniPC',\n ];\n }\n\n private generateDimensionOptions(): Array<{ label: string; value: string }> {\n const options: Array<{ label: string; value: string }> = [];\n for (let size = 256; size <= 2048; size += 64) {\n options.push({\n label: `${size}px`,\n value: String(size),\n });\n }\n return options;\n }\n\n async getParameterSchemas(): Promise<ParameterSchema[]> {\n const samplers = await this.getSamplers();\n const dimensionOptions = this.generateDimensionOptions();\n\n return [\n {\n type: 'select',\n label: 'Sampler',\n variable: 'sampler',\n helpText: 'The sampling method used for image generation',\n defaultValue: 'Euler a',\n selectOptions: samplers.map((name) => ({\n label: name,\n value: name,\n })),\n },\n {\n type: 'select',\n label: 'Width',\n variable: 'width',\n defaultValue: '512',\n selectOptions: dimensionOptions,\n },\n {\n type: 'select',\n label: 'Height',\n variable: 'height',\n defaultValue: '512',\n selectOptions: dimensionOptions,\n },\n {\n type: 'number',\n label: 'Steps',\n variable: 'steps',\n helpText:\n 'Number of denoising steps. More steps = higher quality but slower.',\n defaultValue: '20',\n numberOptions: {\n min: 1,\n max: 150,\n step: 1,\n },\n },\n {\n type: 'number',\n label: 'CFG Scale',\n variable: 'cfgScale',\n helpText:\n 'How strongly the image should follow the prompt. Higher = more literal.',\n defaultValue: '7',\n numberOptions: {\n min: 1,\n max: 30,\n step: 0.5,\n },\n },\n {\n type: 'seed',\n label: 'Seed',\n variable: 'seed',\n helpText:\n \"A specific value used to guide the 'randomness' of generation. Use -1 for random.\",\n defaultValue: '-1',\n },\n {\n type: 'text',\n label: 'Negative Prompt',\n variable: 'negativePrompt',\n helpText: \"Things you don't want in the image\",\n placeholder: 'blurry, low quality, distorted',\n },\n ];\n }\n}\n\nexport default new StableDiffusionProvider();\n","# Stable Diffusion WebUI\n\nAUTOMATIC1111's Stable Diffusion WebUI runs image generation models locally. Once the server is running with at least one model, MindStudio will detect it automatically.\n\n**Default port:** 7860\n**GitHub:** https://github.com/AUTOMATIC1111/stable-diffusion-webui\n\n## What You'll Need\n\n- **Python 3.10 or newer** -- Check by opening a terminal and typing `python3 --version`. If you don't have it, download from https://www.python.org/downloads/\n\n- **Git** -- Check by typing `git --version`. If you don't have it, download from https://git-scm.com/downloads\n\n## Step 1: Install the WebUI\n\nOpen a terminal and paste this command:\n\n```\ngit clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git ~/stable-diffusion-webui\n```\n\n**Windows users**, use this instead:\n\n```\ngit clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git %USERPROFILE%\\stable-diffusion-webui\n```\n\n## Step 2: Download a Model\n\nYou need at least one model file for MindStudio to use. Model files have the `.safetensors` extension (typically 2-7 GB).\n\n1. Browse models at https://civitai.com or https://huggingface.co\n2. Download a `.safetensors` checkpoint file\n3. Move the file into this folder:\n\n```\n~/stable-diffusion-webui/models/Stable-diffusion/\n```\n\nGood starter models:\n\n- **Stable Diffusion XL (SDXL)** -- high quality, 1024x1024\n- **Stable Diffusion 1.5** -- fast, widely supported\n\n## Step 3: Start the Server\n\nOpen a terminal and run:\n\n```\ncd ~/stable-diffusion-webui && ./webui.sh --api\n```\n\n**Windows users:**\n\n```\ncd %USERPROFILE%\\stable-diffusion-webui && webui-user.bat --api\n```\n\nThe first time you run this it will take several minutes to install dependencies. This is normal -- let it finish.\n\n**Important:** The `--api` flag is required. Without it, MindStudio cannot send requests to the server.\n\nLeave this terminal window open while using MindStudio. Once the server is ready, go back to the tunnel and select **Refresh Providers** -- your models should appear.\n\n## Troubleshooting\n\n- **MindStudio says WebUI is \"not running\"** -- Make sure you included `--api` when launching. The terminal should show the server at `http://127.0.0.1:7860`.\n\n- **Server is running but no models show up** -- Make sure your `.safetensors` file is directly in the `models/Stable-diffusion/` folder, not inside a subfolder. Restart the server after adding new model files.\n\n- **\"Python not found\"** -- Python 3.10+ is required. Download from https://www.python.org/downloads/. On Windows, check \"Add Python to PATH\" during installation.\n\n- **Errors during first launch** -- Delete the `venv` folder inside `stable-diffusion-webui` and run the launch command again to reinstall dependencies from scratch.\n\n- **\"CUDA out of memory\"** -- Your GPU doesn't have enough memory. Add `--medvram` or `--lowvram` to the launch command: `./webui.sh --api --medvram`\n","import * as path from 'path';\nimport {\n getProviderBaseUrl,\n setProviderBaseUrl,\n getProviderInstallPath,\n setProviderInstallPath,\n} from '../../config';\nimport { discoverWorkflows } from './workflow-discovery';\nimport { ensureConverterInstalled } from './converter-install';\nimport { executeWorkflow } from './workflow-executor';\nimport readme from './readme.md';\nimport type {\n Provider,\n LocalModel,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImageGenerationProgress,\n VideoGenerationOptions,\n VideoGenerationResult,\n VideoGenerationProgress,\n ProviderSetupStatus,\n} from '../types';\n\n// Desktop app uses 8000, CLI default is 8188\nconst COMFYUI_PORTS = [8000, 8188];\n\n/**\n * ComfyUI provider — discovers user-saved workflows and executes them.\n */\nclass ComfyUIProvider implements Provider {\n readonly name = 'comfyui';\n readonly displayName = 'ComfyUI';\n readonly description =\n 'Run any saved ComfyUI workflow — images, video, and more.';\n readonly capabilities = ['image', 'video'] as const;\n readonly readme = readme;\n readonly defaultBaseUrl = 'http://127.0.0.1:8000';\n\n get baseUrl(): string {\n return getProviderBaseUrl(this.name, this.defaultBaseUrl);\n }\n\n private getBaseUrl(): string {\n return this.baseUrl;\n }\n\n /**\n * Try to reach ComfyUI on the configured URL, then fall back to known ports.\n * Persists whichever URL responds so future calls go direct.\n */\n private async findRunningUrl(): Promise<string | null> {\n // Try configured URL first\n const configured = this.getBaseUrl();\n if (await this.checkUrl(configured)) return configured;\n\n // Try other known ports\n for (const port of COMFYUI_PORTS) {\n const url = `http://127.0.0.1:${port}`;\n if (url === configured) continue; // Already tried\n if (await this.checkUrl(url)) {\n setProviderBaseUrl(this.name, url);\n return url;\n }\n }\n\n return null;\n }\n\n private async checkUrl(url: string): Promise<boolean> {\n try {\n const response = await fetch(`${url}/system_stats`, {\n signal: AbortSignal.timeout(1000),\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Query the running ComfyUI server for its install path via /internal/folder_paths.\n * Derives the root from the custom_nodes path and persists it for future use.\n */\n private async queryInstallPath(baseUrl: string): Promise<string | null> {\n try {\n const response = await fetch(`${baseUrl}/internal/folder_paths`, {\n signal: AbortSignal.timeout(3000),\n });\n if (!response.ok) return null;\n\n const data = (await response.json()) as Record<string, string[]>;\n const customNodesPaths = data.custom_nodes;\n if (!customNodesPaths || customNodesPaths.length === 0) return null;\n\n // custom_nodes path is like /path/to/ComfyUI/custom_nodes — parent is the install root\n const installPath = path.dirname(customNodesPaths[0]!);\n setProviderInstallPath(this.name, installPath);\n return installPath;\n } catch {\n return null;\n }\n }\n\n async isRunning(): Promise<boolean> {\n return (await this.findRunningUrl()) !== null;\n }\n\n async detect(): Promise<ProviderSetupStatus> {\n const runningUrl = await this.findRunningUrl();\n\n if (runningUrl) {\n // Query the server for its install path and auto-install converter\n const queriedPath = await this.queryInstallPath(runningUrl);\n if (queriedPath) {\n ensureConverterInstalled(queriedPath).catch(() => {});\n }\n return { installed: true, running: true };\n }\n\n // Offline: use previously saved path\n const savedPath = getProviderInstallPath(this.name);\n return { installed: !!savedPath, running: false };\n }\n\n /**\n * Discover workflow-based models from user-saved ComfyUI workflows.\n */\n async discoverModels(): Promise<LocalModel[]> {\n const installPath = getProviderInstallPath(this.name) ?? null;\n return discoverWorkflows(this.getBaseUrl(), installPath);\n }\n\n /**\n * Generate an image using a ComfyUI workflow, with progress tracking.\n */\n async generateImage(\n _model: string,\n _prompt: string,\n options?: ImageGenerationOptions,\n onProgress?: (progress: ImageGenerationProgress) => void,\n ): Promise<ImageGenerationResult> {\n if (!options?.workflow) {\n throw new Error('ComfyUI image generation requires a workflow');\n }\n\n const result = await executeWorkflow({\n baseUrl: this.getBaseUrl(),\n workflow: options.workflow,\n onProgress: onProgress\n ? (p) => onProgress({ step: p.step, totalSteps: p.totalSteps })\n : undefined,\n });\n\n return {\n imageBase64: result.dataBase64,\n mimeType: result.mimeType,\n };\n }\n\n /**\n * Generate a video using a ComfyUI workflow.\n */\n async generateVideo(\n _model: string,\n _prompt: string,\n options?: VideoGenerationOptions,\n onProgress?: (progress: VideoGenerationProgress) => void,\n ): Promise<VideoGenerationResult> {\n if (!options?.workflow) {\n throw new Error('ComfyUI video generation requires a workflow');\n }\n\n const result = await executeWorkflow({\n baseUrl: this.getBaseUrl(),\n workflow: options.workflow,\n onProgress: onProgress\n ? (p) =>\n onProgress({\n step: p.step,\n totalSteps: p.totalSteps,\n currentNode: p.currentNode,\n })\n : undefined,\n });\n\n return {\n videoBase64: result.dataBase64,\n mimeType: result.mimeType,\n };\n }\n}\n\nexport default new ComfyUIProvider();\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport type { LocalModel, ComfyWorkflowParameterSchema } from '../types';\nimport {\n ensureConverterInstalled,\n isConverterEndpointAvailable,\n convertWorkflow,\n resetConverterCache,\n} from './converter-install';\n\nconst VIDEO_OUTPUT_NODES = ['VHS_VideoCombine', 'SaveVideo'];\nconst IMAGE_OUTPUT_NODES = ['SaveImage', 'PreviewImage'];\n\n/**\n * Discover user-saved ComfyUI workflows and return them as aggregated LocalModel entries.\n * Returns at most 2 models: \"ComfyUI Image Generation\" and \"ComfyUI Video Generation\",\n * with all discovered workflows bundled in the parameter schema.\n */\nexport async function discoverWorkflows(\n baseUrl: string,\n installPath: string | null,\n): Promise<LocalModel[]> {\n resetConverterCache();\n\n // Try to install converter if we know the install path\n let converterJustInstalled = false;\n if (installPath) {\n const wasInstalled = await ensureConverterInstalled(installPath);\n if (wasInstalled) {\n converterJustInstalled = true;\n }\n }\n\n const converterAvailable = await isConverterEndpointAvailable(baseUrl);\n\n // If we just installed files but endpoint isn't available, ComfyUI needs restart.\n // We still discover API-format workflows that don't need conversion.\n const needsRestart = converterJustInstalled && !converterAvailable;\n\n // List workflow files\n const workflowFiles = await listWorkflowFiles(baseUrl, installPath);\n\n // Collect converted workflows and unconverted counts per capability\n const converted: {\n image: Array<{ name: string; workflow: Record<string, unknown> }>;\n video: Array<{ name: string; workflow: Record<string, unknown> }>;\n } = { image: [], video: [] };\n const unconvertedCapabilities = new Set<'image' | 'video'>();\n\n for (const file of workflowFiles) {\n try {\n const workflowJson = await fetchWorkflowJson(baseUrl, installPath, file);\n if (!workflowJson) continue;\n\n let apiWorkflow: Record<string, unknown>;\n\n if (isApiFormat(workflowJson)) {\n apiWorkflow = workflowJson;\n } else if (converterAvailable) {\n try {\n apiWorkflow = await convertWorkflow(baseUrl, workflowJson);\n } catch {\n continue; // Skip workflows that fail conversion\n }\n } else {\n // Can't convert UI-format without the endpoint — track as unconverted\n unconvertedCapabilities.add('image');\n continue;\n }\n\n const capability = detectCapability(apiWorkflow);\n const name = path.basename(file, path.extname(file));\n converted[capability].push({ name, workflow: apiWorkflow });\n } catch {\n // Silent failure per-workflow — one broken workflow doesn't block others\n }\n }\n\n const models: LocalModel[] = [];\n\n // Emit aggregated models for each capability that has converted workflows\n for (const capability of ['image', 'video'] as const) {\n if (converted[capability].length > 0) {\n const displayName =\n capability === 'image'\n ? 'ComfyUI Image Generation'\n : 'ComfyUI Video Generation';\n const workflowParam: ComfyWorkflowParameterSchema = {\n type: 'comfyWorkflow',\n variable: 'workflow',\n label: 'Workflow',\n comfyWorkflowOptions: { availableWorkflows: converted[capability] },\n };\n models.push({\n name: displayName,\n provider: 'comfyui',\n capability,\n parameters: [workflowParam],\n });\n }\n }\n\n // Emit statusHint models for capabilities that have only unconverted workflows\n for (const capability of unconvertedCapabilities) {\n // Skip if we already have converted workflows for this capability\n if (converted[capability].length > 0) continue;\n\n const displayName =\n capability === 'image'\n ? 'ComfyUI Image Generation'\n : 'ComfyUI Video Generation';\n models.push({\n name: displayName,\n provider: 'comfyui',\n capability,\n statusHint: needsRestart\n ? 'Restart ComfyUI to enable'\n : 'Workflow converter not available',\n });\n }\n\n return models;\n}\n\n/**\n * List workflow files via the ComfyUI userdata API, falling back to filesystem.\n */\nasync function listWorkflowFiles(\n baseUrl: string,\n installPath: string | null,\n): Promise<string[]> {\n // Try API first\n try {\n const response = await fetch(\n `${baseUrl}/userdata?dir=workflows/&recurse=true&full_info=true`,\n { signal: AbortSignal.timeout(5000) },\n );\n if (response.ok) {\n const data = (await response.json()) as Array<string | { path: string }>;\n return data\n .map((entry) => (typeof entry === 'string' ? entry : entry.path))\n .filter((p) => p.endsWith('.json'));\n }\n } catch {\n // Fall through to filesystem\n }\n\n // Filesystem fallback\n if (installPath) {\n const workflowsDir = path.join(installPath, 'user', 'default', 'workflows');\n return scanDirectory(workflowsDir);\n }\n\n return [];\n}\n\n/**\n * Recursively scan a directory for .json files.\n */\nfunction scanDirectory(dir: string): string[] {\n if (!fs.existsSync(dir)) return [];\n\n const results: string[] = [];\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(\n ...scanDirectory(fullPath).map((f) => path.join(entry.name, f)),\n );\n } else if (entry.name.endsWith('.json')) {\n results.push(entry.name);\n }\n }\n } catch {\n // Ignore read errors\n }\n return results;\n}\n\n/**\n * Fetch a workflow's JSON content via the userdata API, falling back to filesystem.\n */\nasync function fetchWorkflowJson(\n baseUrl: string,\n installPath: string | null,\n filePath: string,\n): Promise<Record<string, unknown> | null> {\n // Try API first — the {file} param needs the full path from the userdata root,\n // with slashes encoded (aiohttp matches {file} as a single path segment)\n try {\n const userdataPath = `workflows/${filePath}`;\n const response = await fetch(\n `${baseUrl}/userdata/${encodeURIComponent(userdataPath)}`,\n { signal: AbortSignal.timeout(5000) },\n );\n if (response.ok) {\n return (await response.json()) as Record<string, unknown>;\n }\n } catch {\n // Fall through to filesystem\n }\n\n // Filesystem fallback\n if (installPath) {\n const fullPath = path.join(\n installPath,\n 'user',\n 'default',\n 'workflows',\n filePath,\n );\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n return JSON.parse(content) as Record<string, unknown>;\n } catch {\n return null;\n }\n }\n\n return null;\n}\n\n/**\n * Check if a workflow JSON is already in API format.\n * API format has numeric string keys with objects containing `class_type`.\n */\nexport function isApiFormat(json: Record<string, unknown>): boolean {\n const keys = Object.keys(json);\n if (keys.length === 0) return false;\n\n // API format: keys are numeric strings, values have class_type\n return keys.some((key) => {\n const node = json[key];\n return (\n /^\\d+$/.test(key) &&\n typeof node === 'object' &&\n node !== null &&\n 'class_type' in node\n );\n });\n}\n\n/**\n * Detect whether a workflow produces image or video output.\n */\nexport function detectCapability(\n apiWorkflow: Record<string, unknown>,\n): 'image' | 'video' {\n for (const node of Object.values(apiWorkflow)) {\n if (typeof node === 'object' && node !== null && 'class_type' in node) {\n const classType = (node as { class_type: string }).class_type;\n if (VIDEO_OUTPUT_NODES.includes(classType)) {\n return 'video';\n }\n }\n }\n\n for (const node of Object.values(apiWorkflow)) {\n if (typeof node === 'object' && node !== null && 'class_type' in node) {\n const classType = (node as { class_type: string }).class_type;\n if (IMAGE_OUTPUT_NODES.includes(classType)) {\n return 'image';\n }\n }\n }\n\n // Default to image if we can't determine\n return 'image';\n}\n","import * as fs from 'fs';\nimport * as path from 'path';\n\nconst CONVERTER_DIR = 'comfyui-workflow-to-api-converter-endpoint';\nconst GITHUB_RAW_BASE =\n 'https://raw.githubusercontent.com/SethRobinson/comfyui-workflow-to-api-converter-endpoint/main';\nconst FILES_TO_DOWNLOAD = ['__init__.py', 'workflow_converter.py'];\n\n/**\n * Ensure the workflow converter custom node is installed in ComfyUI's custom_nodes directory.\n * Returns true if files are on disk (ComfyUI may still need a restart to load the endpoint).\n */\nexport async function ensureConverterInstalled(\n installPath: string,\n): Promise<boolean> {\n const customNodesDir = path.join(installPath, 'custom_nodes');\n const converterDir = path.join(customNodesDir, CONVERTER_DIR);\n\n // Check if already installed\n const allFilesExist = FILES_TO_DOWNLOAD.every((f) =>\n fs.existsSync(path.join(converterDir, f)),\n );\n if (allFilesExist) {\n return true;\n }\n\n // Ensure custom_nodes directory exists\n if (!fs.existsSync(customNodesDir)) {\n return false; // Not a valid ComfyUI install\n }\n\n try {\n if (!fs.existsSync(converterDir)) {\n fs.mkdirSync(converterDir, { recursive: true });\n }\n\n for (const filename of FILES_TO_DOWNLOAD) {\n const url = `${GITHUB_RAW_BASE}/${filename}`;\n const response = await fetch(url, {\n signal: AbortSignal.timeout(15000),\n });\n if (!response.ok) {\n throw new Error(`Failed to download ${filename}: ${response.status}`);\n }\n const content = await response.text();\n fs.writeFileSync(path.join(converterDir, filename), content, 'utf-8');\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nlet converterAvailableCache: boolean | null = null;\n\n/**\n * Check if the /workflow/convert endpoint is available on the running ComfyUI server.\n * Caches result for the duration of the discovery run.\n */\nexport async function isConverterEndpointAvailable(\n baseUrl: string,\n): Promise<boolean> {\n if (converterAvailableCache !== null) {\n return converterAvailableCache;\n }\n\n try {\n const response = await fetch(`${baseUrl}/workflow/convert`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ nodes: [], links: [] }),\n signal: AbortSignal.timeout(5000),\n });\n // Only a 200 means the endpoint is actually loaded;\n // ComfyUI returns 405 for unknown routes, not 404\n converterAvailableCache = response.ok;\n return converterAvailableCache;\n } catch {\n converterAvailableCache = false;\n return false;\n }\n}\n\n/**\n * Reset the converter availability cache. Call at the start of each discovery run.\n */\nexport function resetConverterCache(): void {\n converterAvailableCache = null;\n}\n\n/**\n * Convert a UI-format workflow to API format using ComfyUI's /workflow/convert endpoint.\n * The endpoint auto-detects if already API format and passes through.\n */\nexport async function convertWorkflow(\n baseUrl: string,\n uiWorkflow: unknown,\n): Promise<Record<string, unknown>> {\n const response = await fetch(`${baseUrl}/workflow/convert`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(uiWorkflow),\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Workflow conversion failed: ${response.status} ${errorText}`);\n }\n\n return (await response.json()) as Record<string, unknown>;\n}\n","import * as path from 'path';\n\nexport interface WorkflowExecutionResult {\n dataBase64: string;\n mimeType: string;\n filename: string;\n}\n\nexport interface WorkflowExecutionProgress {\n step: number;\n totalSteps: number;\n currentNode?: string;\n}\n\ninterface OutputFile {\n filename: string;\n subfolder: string;\n type: string;\n}\n\n/**\n * Execute an arbitrary workflow on ComfyUI and return the first output.\n * Handles: POST /prompt → WebSocket progress → GET /history → GET /view\n */\nexport async function executeWorkflow(options: {\n baseUrl: string;\n workflow: Record<string, unknown>;\n onProgress?: (progress: WorkflowExecutionProgress) => void;\n}): Promise<WorkflowExecutionResult> {\n const { baseUrl, workflow, onProgress } = options;\n\n const clientId = `mindstudio_${Date.now()}_${Math.random().toString(36).slice(2)}`;\n const wsUrl = baseUrl.replace(/^http/, 'ws') + `/ws?clientId=${clientId}`;\n\n // Submit prompt\n const submitResponse = await fetch(`${baseUrl}/prompt`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n prompt: workflow,\n client_id: clientId,\n }),\n });\n\n if (!submitResponse.ok) {\n const errorText = await submitResponse.text();\n throw new Error(\n `ComfyUI prompt submission failed: ${submitResponse.status} ${errorText}`,\n );\n }\n\n const submitResult = (await submitResponse.json()) as {\n prompt_id: string;\n number: number;\n node_errors?: Record<string, unknown>;\n };\n\n if (\n submitResult.node_errors &&\n Object.keys(submitResult.node_errors).length > 0\n ) {\n throw new Error(\n `ComfyUI workflow validation failed: ${JSON.stringify(submitResult.node_errors)}`,\n );\n }\n\n const promptId = submitResult.prompt_id;\n\n // Wait for completion via WebSocket\n await waitForCompletion(wsUrl, promptId, onProgress);\n\n // Fetch history\n const historyResponse = await fetch(`${baseUrl}/history/${promptId}`, {\n signal: AbortSignal.timeout(30000),\n });\n\n if (!historyResponse.ok) {\n throw new Error(\n `Failed to fetch result history: ${historyResponse.status}`,\n );\n }\n\n const history = (await historyResponse.json()) as Record<\n string,\n {\n outputs: Record<\n string,\n {\n images?: OutputFile[];\n gifs?: OutputFile[];\n }\n >;\n }\n >;\n\n const promptHistory = history[promptId];\n if (!promptHistory) {\n throw new Error('No result found in ComfyUI history');\n }\n\n // Scan ALL output nodes — prefer gifs (video) over images\n let outputFile: OutputFile | null = null;\n\n for (const nodeOutputs of Object.values(promptHistory.outputs)) {\n if (nodeOutputs.gifs && nodeOutputs.gifs.length > 0) {\n outputFile = nodeOutputs.gifs[0]!;\n break; // Prefer video output\n }\n if (!outputFile && nodeOutputs.images && nodeOutputs.images.length > 0) {\n outputFile = nodeOutputs.images[0]!;\n }\n }\n\n if (!outputFile) {\n throw new Error('No output files found in ComfyUI result');\n }\n\n // Download the output file\n const fileUrl = new URL(`${baseUrl}/view`);\n fileUrl.searchParams.set('filename', outputFile.filename);\n fileUrl.searchParams.set('subfolder', outputFile.subfolder || '');\n fileUrl.searchParams.set('type', outputFile.type || 'output');\n\n const fileResponse = await fetch(fileUrl.toString(), {\n signal: AbortSignal.timeout(60000),\n });\n\n if (!fileResponse.ok) {\n throw new Error(`Failed to download output file: ${fileResponse.status}`);\n }\n\n const fileBuffer = await fileResponse.arrayBuffer();\n const dataBase64 = Buffer.from(fileBuffer).toString('base64');\n\n const ext = path.extname(outputFile.filename).toLowerCase();\n const mimeType = getMimeType(ext);\n\n return { dataBase64, mimeType, filename: outputFile.filename };\n}\n\nfunction getMimeType(ext: string): string {\n switch (ext) {\n case '.mp4':\n return 'video/mp4';\n case '.webm':\n return 'video/webm';\n case '.webp':\n return 'image/webp';\n case '.gif':\n return 'image/gif';\n case '.png':\n return 'image/png';\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg';\n default:\n return 'application/octet-stream';\n }\n}\n\nfunction waitForCompletion(\n wsUrl: string,\n promptId: string,\n onProgress?: (progress: WorkflowExecutionProgress) => void,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const timeoutMs = 30 * 60 * 1000;\n let ws: WebSocket;\n\n const timeout = setTimeout(() => {\n try {\n ws?.close();\n } catch {\n // Ignore\n }\n reject(new Error('Workflow execution timed out after 30 minutes'));\n }, timeoutMs);\n\n try {\n ws = new WebSocket(wsUrl);\n } catch (err) {\n clearTimeout(timeout);\n reject(\n new Error(\n `Failed to connect to ComfyUI WebSocket: ${err instanceof Error ? err.message : err}`,\n ),\n );\n return;\n }\n\n ws.onmessage = (event) => {\n try {\n const message = JSON.parse(\n typeof event.data === 'string' ? event.data : '',\n ) as {\n type: string;\n data: Record<string, unknown>;\n };\n\n if (message.type === 'progress') {\n const data = message.data as {\n value: number;\n max: number;\n prompt_id?: string;\n node?: string;\n };\n if (!data.prompt_id || data.prompt_id === promptId) {\n onProgress?.({\n step: data.value,\n totalSteps: data.max,\n currentNode: data.node as string | undefined,\n });\n }\n }\n\n if (message.type === 'execution_success') {\n const data = message.data as { prompt_id: string };\n if (data.prompt_id === promptId) {\n clearTimeout(timeout);\n ws.close();\n resolve();\n }\n }\n\n if (message.type === 'execution_error') {\n const data = message.data as {\n prompt_id: string;\n exception_message?: string;\n node_type?: string;\n };\n if (data.prompt_id === promptId) {\n clearTimeout(timeout);\n ws.close();\n reject(\n new Error(\n `ComfyUI execution error${data.node_type ? ` in ${data.node_type}` : ''}: ${data.exception_message || 'Unknown error'}`,\n ),\n );\n }\n }\n } catch {\n // Ignore non-JSON messages\n }\n };\n\n ws.onerror = () => {\n clearTimeout(timeout);\n reject(new Error('ComfyUI WebSocket error: connection failed'));\n };\n\n ws.onclose = (event) => {\n if (!event.wasClean) {\n clearTimeout(timeout);\n reject(new Error('ComfyUI WebSocket connection closed unexpectedly'));\n }\n };\n });\n}\n","# ComfyUI\n\nComfyUI is a node-based workflow tool for running image and video generation models locally. MindStudio automatically discovers your saved workflows and makes them available as models -- any workflow you build or download in ComfyUI can be used through MindStudio.\n\n**Default port:** 8188\n**Website:** https://www.comfy.org\n\n## What You'll Need\n\n- **A GPU with 8+ GB of VRAM** -- Image and video generation is demanding. Without enough GPU memory, generation will fail or be extremely slow.\n\n## Step 1: Install ComfyUI\n\nDownload and install ComfyUI Desktop from the official website:\n\nhttps://www.comfy.org/download\n\nThe installer handles Python, dependencies, and everything else automatically. Follow the on-screen prompts to complete setup.\n\n## Step 2: Save a Workflow\n\nMindStudio discovers workflows you've saved in ComfyUI. If you don't have any saved workflows yet, open the ComfyUI interface in your browser (http://127.0.0.1:8188), build or load a workflow, and save it using the menu. Downloaded workflow files placed in ComfyUI's workflows folder will also be discovered.\n\nAny workflow that produces image or video output will work. MindStudio detects the output type automatically based on the nodes in your workflow.\n\n## Step 3: Start the Server\n\nOpen ComfyUI Desktop. Once it's running, go back to the tunnel and select **Refresh Providers** -- your saved workflows should appear as models.\n\nIf you're running ComfyUI from the command line instead, start it with:\n\n```\ncd ~/ComfyUI && python main.py --listen\n```\n\n**Important:** The `--listen` flag is required when running from the command line. Without it, MindStudio cannot connect to the server.\n\n## Tip: Workflow Converter\n\nMindStudio automatically installs a custom node called `comfyui-workflow-to-api-converter-endpoint` into ComfyUI's `custom_nodes/` folder. This converts workflows saved in ComfyUI's UI format into the API format needed for execution. **After the first run, you'll need to restart ComfyUI once** so it picks up the new node — after that, it works automatically. If the auto-install doesn't work (e.g. permissions issues), you can install it manually by cloning https://github.com/SethRobinson/comfyui-workflow-to-api-converter-endpoint into your ComfyUI `custom_nodes/` directory and restarting ComfyUI. Without this node, only workflows already saved in API format will be discovered.\n\n## Troubleshooting\n\n- **MindStudio says ComfyUI is \"not running\"** -- Make sure ComfyUI Desktop is open, or if running from the terminal, that you started with the `--listen` flag.\n\n- **Server is running but no workflows show up** -- Make sure you have at least one saved workflow in ComfyUI. Open the ComfyUI interface, load or build a workflow, and save it.\n\n- **\"CUDA out of memory\"** -- Your GPU doesn't have enough memory for the workflow you're running. Try a lighter model or reduce resolution in your workflow.\n\n- **Server crashes mid-generation** -- Restart ComfyUI Desktop, or press Ctrl+C in the terminal and run the start command again.\n","import ollama from './ollama';\nimport lmstudio from './lmstudio';\nimport stableDiffusion from './stable-diffusion';\nimport comfyui from './comfyui';\nimport type {\n Provider,\n LocalModel,\n ProviderSetupStatus,\n ModelCapability,\n} from './types';\n\nexport * from './types';\n\n// Registry of all available providers\nexport const allProviders: Provider[] = [\n ollama,\n lmstudio,\n stableDiffusion,\n comfyui,\n];\n\n/**\n * Get a provider instance by name\n */\nexport function getProvider(name: string): Provider | undefined {\n return allProviders.find((p) => p.name === name);\n}\n\n/**\n * Get all providers that support a given capability\n */\nexport function getProvidersByCapability(cap: ModelCapability): Provider[] {\n return allProviders.filter((p) => p.capabilities.includes(cap));\n}\n\n/**\n * Discover which providers are currently running\n */\nexport async function discoverRunningProviders(): Promise<Provider[]> {\n const results = await Promise.all(\n allProviders.map(async (provider) => ({\n provider,\n running: await provider.isRunning(),\n })),\n );\n\n return results.filter((r) => r.running).map((r) => r.provider);\n}\n\n/**\n * Discover all models from all running providers\n */\nexport async function discoverAllModels(): Promise<LocalModel[]> {\n const runningProviders = await discoverRunningProviders();\n\n const modelArrays = await Promise.all(\n runningProviders.map((p) => p.discoverModels()),\n );\n\n return modelArrays.flat();\n}\n\n/**\n * Check if any provider is running\n */\nexport async function isAnyProviderRunning(): Promise<boolean> {\n const results = await Promise.all(allProviders.map((p) => p.isRunning()));\n return results.some((r) => r);\n}\n\n/**\n * Get provider status for all providers\n */\nexport async function getProviderStatuses(): Promise<\n Array<{ provider: Provider; running: boolean }>\n> {\n return Promise.all(\n allProviders.map(async (provider) => ({\n provider,\n running: await provider.isRunning(),\n })),\n );\n}\n\n/**\n * Discover models filtered by capability\n */\nexport async function discoverModelsByCapability(\n capability: ModelCapability,\n): Promise<LocalModel[]> {\n const runningProviders = await discoverRunningProviders();\n const filteredProviders = runningProviders.filter((p) =>\n p.capabilities.includes(capability),\n );\n\n const modelArrays = await Promise.all(\n filteredProviders.map((p) => p.discoverModels()),\n );\n\n return modelArrays.flat();\n}\n\n/**\n * Detect installation/running status for all providers\n */\nexport async function detectAllProviderStatuses(): Promise<\n Array<{ provider: Provider; status: ProviderSetupStatus }>\n> {\n return Promise.all(\n allProviders.map(async (provider) => ({\n provider,\n status: await provider.detect(),\n })),\n );\n}\n\n/**\n * Discover all models with their parameter schemas\n * For providers with getParameterSchemas, fetches available parameters dynamically\n */\nexport async function discoverAllModelsWithParameters(): Promise<LocalModel[]> {\n const runningProviders = await discoverRunningProviders();\n\n const modelsWithParams = await Promise.all(\n runningProviders.map(async (provider) => {\n const models = await provider.discoverModels();\n\n // Filter out status hint entries — they're for TUI display only\n const realModels = models.filter((m) => !m.statusHint);\n\n if (typeof provider.getParameterSchemas === 'function') {\n const parameters = await provider.getParameterSchemas();\n return realModels.map((model) => ({\n ...model,\n parameters: model.parameters ?? parameters,\n }));\n }\n\n return realModels;\n }),\n );\n\n return modelsWithParams.flat();\n}\n","import { EventEmitter } from 'events';\n\nexport interface RequestStartEvent {\n id: string;\n modelId: string;\n requestType: 'llm_chat' | 'image_generation' | 'video_generation';\n timestamp: number;\n}\n\nexport interface RequestProgressEvent {\n id: string;\n content?: string;\n step?: number;\n totalSteps?: number;\n}\n\nexport interface RequestCompleteEvent {\n id: string;\n success: boolean;\n duration: number;\n result?: {\n chars?: number;\n imageSize?: number;\n videoSize?: number;\n };\n error?: string;\n}\n\nclass RequestEventEmitter extends EventEmitter {\n emitStart(event: RequestStartEvent) {\n this.emit('request:start', event);\n }\n\n emitProgress(event: RequestProgressEvent) {\n this.emit('request:progress', event);\n }\n\n emitComplete(event: RequestCompleteEvent) {\n this.emit('request:complete', event);\n }\n\n onStart(handler: (event: RequestStartEvent) => void) {\n this.on('request:start', handler);\n return () => this.off('request:start', handler);\n }\n\n onProgress(handler: (event: RequestProgressEvent) => void) {\n this.on('request:progress', handler);\n return () => this.off('request:progress', handler);\n }\n\n onComplete(handler: (event: RequestCompleteEvent) => void) {\n this.on('request:complete', handler);\n return () => this.off('request:complete', handler);\n }\n}\n\nexport const requestEvents = new RequestEventEmitter();\n","import {\n pollForRequest,\n submitProgress,\n submitResult,\n disconnectHeartbeat,\n type LocalModelRequest,\n type SyncedModel,\n} from './api';\nimport {\n getProvider,\n discoverAllModels,\n type Provider,\n type LocalModel,\n} from './providers';\nimport { requestEvents } from './events';\n\ninterface ModelMapping {\n provider: Provider;\n localModelName: string;\n}\n\n/**\n * TunnelRunner handles the polling and request processing loop.\n * It emits events that listeners (TUI or simple chalk output) can subscribe to.\n */\nexport class TunnelRunner {\n private isRunning = false;\n private modelMap: Map<string, ModelMapping> = new Map();\n private modelIds: string[] = [];\n\n /**\n * Start with a pre-discovered list of synced models.\n * Used by the TUI, which discovers models itself.\n */\n async start(syncedModels: SyncedModel[]): Promise<void> {\n if (this.isRunning) return;\n\n this.modelIds = syncedModels.map((m) => m.id);\n this.isRunning = true;\n\n // Build cloud ID -> { provider, localModelName } mapping\n const allModels = await discoverAllModels();\n this.buildModelMap(syncedModels, allModels);\n\n // Start polling loop\n this.pollLoop();\n }\n\n stop(): void {\n this.isRunning = false;\n disconnectHeartbeat().catch(() => {});\n }\n\n private buildModelMap(\n syncedModels: SyncedModel[],\n localModels: LocalModel[],\n ): void {\n this.modelMap.clear();\n // Index local models by name for fast lookup\n const localByName = new Map<string, LocalModel>();\n for (const model of localModels) {\n localByName.set(model.name, model);\n }\n // Map cloud ID -> provider + local model name\n for (const synced of syncedModels) {\n const local = localByName.get(synced.name);\n if (local) {\n const provider = getProvider(local.provider);\n if (provider) {\n this.modelMap.set(synced.id, {\n provider,\n localModelName: local.name,\n });\n }\n }\n }\n }\n\n private async pollLoop(): Promise<void> {\n while (this.isRunning) {\n try {\n const request = await pollForRequest(this.modelIds);\n if (request) {\n // Process request in background\n this.processRequest(request);\n }\n } catch (error) {\n // Wait before retrying on error\n await this.sleep(5000);\n }\n }\n }\n\n private async processRequest(request: LocalModelRequest): Promise<void> {\n const startTime = Date.now();\n\n // Emit start event\n requestEvents.emitStart({\n id: request.id,\n modelId: request.modelId,\n requestType: request.requestType,\n timestamp: startTime,\n });\n\n const mapping = this.modelMap.get(request.modelId);\n\n if (!mapping) {\n const error = `Model ${request.modelId} not found`;\n await submitResult(request.id, false, undefined, error);\n requestEvents.emitComplete({\n id: request.id,\n success: false,\n duration: Date.now() - startTime,\n error,\n });\n return;\n }\n\n try {\n switch (request.requestType) {\n case 'llm_chat':\n await this.handleTextRequest(request, mapping, startTime);\n break;\n case 'image_generation':\n await this.handleImageRequest(request, mapping, startTime);\n break;\n case 'video_generation':\n await this.handleVideoRequest(request, mapping, startTime);\n break;\n default:\n throw new Error(`Unsupported request type: ${request.requestType}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n await submitResult(request.id, false, undefined, message);\n requestEvents.emitComplete({\n id: request.id,\n success: false,\n duration: Date.now() - startTime,\n error: message,\n });\n }\n }\n\n private async handleTextRequest(\n request: LocalModelRequest,\n { provider, localModelName }: ModelMapping,\n startTime: number,\n ): Promise<void> {\n if (!provider.chat) {\n throw new Error(`Provider does not support text generation`);\n }\n\n const messages = (request.payload.messages || []).map((m) => ({\n role: m.role as 'user' | 'assistant' | 'system',\n content: m.content,\n }));\n\n const stream = provider.chat(localModelName, messages, {\n temperature: request.payload.temperature,\n maxTokens: request.payload.maxTokens,\n });\n\n let fullContent = '';\n let lastProgressUpdate = 0;\n const progressInterval = 100;\n\n for await (const chunk of stream) {\n fullContent += chunk.content;\n\n const now = Date.now();\n if (now - lastProgressUpdate > progressInterval) {\n await submitProgress(request.id, fullContent);\n requestEvents.emitProgress({\n id: request.id,\n content: fullContent,\n });\n lastProgressUpdate = now;\n }\n }\n\n await submitProgress(request.id, fullContent);\n await submitResult(request.id, true, {\n content: fullContent,\n usage: { promptTokens: 0, completionTokens: 0 },\n });\n\n requestEvents.emitComplete({\n id: request.id,\n success: true,\n duration: Date.now() - startTime,\n result: { chars: fullContent.length },\n });\n }\n\n private async handleImageRequest(\n request: LocalModelRequest,\n { provider, localModelName }: ModelMapping,\n startTime: number,\n ): Promise<void> {\n if (!provider.generateImage) {\n throw new Error(`Provider does not support image generation`);\n }\n\n const prompt = request.payload.prompt || '';\n const config = request.payload.config || {};\n\n const result = await provider.generateImage(\n localModelName,\n prompt,\n {\n negativePrompt: config.negativePrompt as string | undefined,\n width: config.width as number | undefined,\n height: config.height as number | undefined,\n steps: config.steps as number | undefined,\n cfgScale: config.cfgScale as number | undefined,\n seed: config.seed as number | undefined,\n sampler: config.sampler as string | undefined,\n workflow: config.workflow as Record<string, unknown> | undefined,\n },\n async (progress) => {\n await submitProgress(\n request.id,\n `Step ${progress.step}/${progress.totalSteps}`,\n 'log',\n );\n requestEvents.emitProgress({\n id: request.id,\n step: progress.step,\n totalSteps: progress.totalSteps,\n });\n },\n );\n\n await submitResult(request.id, true, {\n imageBase64: result.imageBase64,\n mimeType: result.mimeType,\n seed: result.seed,\n });\n\n const imageSize = Math.round((result.imageBase64.length * 3) / 4);\n\n requestEvents.emitComplete({\n id: request.id,\n success: true,\n duration: Date.now() - startTime,\n result: { imageSize },\n });\n }\n\n private async handleVideoRequest(\n request: LocalModelRequest,\n { provider, localModelName }: ModelMapping,\n startTime: number,\n ): Promise<void> {\n if (!provider.generateVideo) {\n throw new Error(`Provider does not support video generation`);\n }\n\n const prompt = request.payload.prompt || '';\n const config = request.payload.config || {};\n\n const result = await provider.generateVideo(\n localModelName,\n prompt,\n {\n negativePrompt: config.negativePrompt as string | undefined,\n width: config.width as number | undefined,\n height: config.height as number | undefined,\n numFrames: config.numFrames as number | undefined,\n fps: config.fps as number | undefined,\n steps: config.steps as number | undefined,\n cfgScale: config.cfgScale as number | undefined,\n seed: config.seed as number | undefined,\n workflow: config.workflow as Record<string, unknown> | undefined,\n },\n async (progress) => {\n await submitProgress(\n request.id,\n `Step ${progress.step}/${progress.totalSteps}`,\n 'log',\n );\n requestEvents.emitProgress({\n id: request.id,\n step: progress.step,\n totalSteps: progress.totalSteps,\n });\n },\n );\n\n await submitResult(request.id, true, {\n videoBase64: result.videoBase64,\n mimeType: result.mimeType,\n duration: result.duration,\n fps: result.fps,\n seed: result.seed,\n });\n\n const videoSize = Math.round((result.videoBase64.length * 3) / 4);\n\n requestEvents.emitComplete({\n id: request.id,\n success: true,\n duration: Date.now() - startTime,\n result: { videoSize },\n });\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["path","os","readme_default","readme_default","fs","path","os","readme_default","readme_default","response","path","fs","path","fs","path","path","submitResult","readme_default","readme_default","config"]}
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  async function main() {
5
- const { startTUI } = await import("./tui-KQ4LWB4E.js");
5
+ const { startTUI } = await import("./tui-BW6XKMWK.js");
6
6
  await startTUI();
7
7
  process.exit(0);
8
8
  }
package/dist/index.d.ts CHANGED
@@ -1,18 +1,23 @@
1
+ interface SyncedModel {
2
+ id: string;
3
+ name: string;
4
+ }
5
+
1
6
  /**
2
7
  * TunnelRunner handles the polling and request processing loop.
3
8
  * It emits events that listeners (TUI or simple chalk output) can subscribe to.
4
9
  */
5
10
  declare class TunnelRunner {
6
11
  private isRunning;
7
- private modelProviderMap;
8
- private models;
12
+ private modelMap;
13
+ private modelIds;
9
14
  /**
10
- * Start with a pre-discovered list of model names.
15
+ * Start with a pre-discovered list of synced models.
11
16
  * Used by the TUI, which discovers models itself.
12
17
  */
13
- start(models: string[]): Promise<void>;
18
+ start(syncedModels: SyncedModel[]): Promise<void>;
14
19
  stop(): void;
15
- private buildModelProviderMap;
20
+ private buildModelMap;
16
21
  private pollLoop;
17
22
  private processRequest;
18
23
  private handleTextRequest;