@mindstudio-ai/agent 0.0.16 → 0.0.18
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 +24 -11
- package/dist/cli.js +433 -13
- package/dist/index.d.ts +247 -15
- package/dist/index.js +90 -13
- package/dist/index.js.map +1 -1
- package/llms.txt +100 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ Every method is fully typed — your editor will autocomplete available paramete
|
|
|
46
46
|
### CLI
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
#
|
|
50
|
-
|
|
49
|
+
# Authenticate (opens browser, saves key locally)
|
|
50
|
+
mindstudio login
|
|
51
51
|
|
|
52
52
|
# Execute a step with named flags
|
|
53
53
|
mindstudio generate-image --prompt "A mountain landscape at sunset"
|
|
@@ -96,9 +96,15 @@ All 120+ step methods are exposed as MCP tools with full JSON Schema input defin
|
|
|
96
96
|
|
|
97
97
|
## Authentication
|
|
98
98
|
|
|
99
|
-
The
|
|
99
|
+
The fastest way to authenticate is the interactive login:
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
```bash
|
|
102
|
+
mindstudio login
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This opens your browser, authenticates with MindStudio, and saves your API key to `~/.mindstudio/config.json`. All subsequent CLI and SDK usage will pick it up automatically.
|
|
106
|
+
|
|
107
|
+
You can also authenticate via environment variable or constructor parameter:
|
|
102
108
|
|
|
103
109
|
```typescript
|
|
104
110
|
// Pass directly
|
|
@@ -109,15 +115,19 @@ const agent = new MindStudioAgent({ apiKey: 'your-api-key' });
|
|
|
109
115
|
const agent = new MindStudioAgent();
|
|
110
116
|
```
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
MindStudio routes to the correct AI provider (OpenAI, Google, Anthropic, etc.) server-side — you do not need separate provider API keys.
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
Other auth commands:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Check current auth status and verify credentials
|
|
124
|
+
mindstudio whoami
|
|
125
|
+
|
|
126
|
+
# Clear stored credentials
|
|
127
|
+
mindstudio logout
|
|
118
128
|
```
|
|
119
129
|
|
|
120
|
-
Resolution order: constructor `apiKey` > `MINDSTUDIO_API_KEY` env > `CALLBACK_TOKEN` env.
|
|
130
|
+
Resolution order: constructor `apiKey` > `MINDSTUDIO_API_KEY` env > `~/.mindstudio/config.json` > `CALLBACK_TOKEN` env.
|
|
121
131
|
|
|
122
132
|
## Thread persistence
|
|
123
133
|
|
|
@@ -248,7 +258,7 @@ const { services } = await agent.listConnectors();
|
|
|
248
258
|
|
|
249
259
|
```typescript
|
|
250
260
|
const agent = new MindStudioAgent({
|
|
251
|
-
// API key (or set MINDSTUDIO_API_KEY env var)
|
|
261
|
+
// API key (or set MINDSTUDIO_API_KEY env var, or run `mindstudio login`)
|
|
252
262
|
apiKey: 'your-api-key',
|
|
253
263
|
|
|
254
264
|
// Base URL (or set MINDSTUDIO_BASE_URL env var)
|
|
@@ -322,6 +332,9 @@ import { blockTypeAliases } from '@mindstudio-ai/agent';
|
|
|
322
332
|
Usage: mindstudio <command | method> [options]
|
|
323
333
|
|
|
324
334
|
Commands:
|
|
335
|
+
login Authenticate with MindStudio (opens browser)
|
|
336
|
+
logout Clear stored credentials
|
|
337
|
+
whoami Show current authentication status
|
|
325
338
|
<method> [json | --flags] Execute a step method
|
|
326
339
|
exec <method> [json | --flags] Execute a step method (same as above)
|
|
327
340
|
list [--json] List available methods
|
package/dist/cli.js
CHANGED
|
@@ -201,6 +201,27 @@ var init_metadata = __esm({
|
|
|
201
201
|
inputSchema: { "type": "object", "properties": { "input": { "type": "string", "description": "Text to scan for personally identifiable information" }, "language": { "type": "string", "description": 'Language code of the input text (e.g. "en")' }, "entities": { "type": "array", "items": { "type": "string" }, "description": 'PII entity types to scan for (e.g. ["PHONE_NUMBER", "EMAIL_ADDRESS"]). Empty array means nothing is scanned.' }, "detectedStepId": { "type": "string", "description": "Step to transition to if PII is detected (workflow mode)" }, "notDetectedStepId": { "type": "string", "description": "Step to transition to if no PII is detected (workflow mode)" }, "outputLogVariable": { "type": "string", "description": "Variable name to store the raw detection results" } }, "required": ["input", "language", "entities"] },
|
|
202
202
|
outputSchema: { "type": "object", "properties": { "detected": { "type": "boolean", "description": "Whether any PII was found in the input text" }, "detections": { "type": "array", "items": { "type": "object", "properties": { "entity_type": { "type": "string", "description": 'PII entity type (e.g. "PHONE_NUMBER", "EMAIL_ADDRESS", "PERSON")' }, "start": { "type": "number", "description": "Start character index in the input text" }, "end": { "type": "number", "description": "End character index in the input text" }, "score": { "type": "number", "description": "Confidence score between 0 and 1" } }, "required": ["entity_type", "start", "end", "score"] }, "description": "List of detected PII entities with type, location, and confidence" } }, "required": ["detected", "detections"] }
|
|
203
203
|
},
|
|
204
|
+
"discordEditMessage": {
|
|
205
|
+
stepType: "discordEditMessage",
|
|
206
|
+
description: "Edit a previously sent Discord channel message. Use with the message ID returned by Send Discord Message.",
|
|
207
|
+
usageNotes: "- Only messages sent by the bot can be edited.\n- The messageId is returned by the Send Discord Message step.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- When editing with an attachment, the new attachment replaces any previous attachments on the message.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).",
|
|
208
|
+
inputSchema: { "type": "object", "properties": { "botToken": { "type": "string", "description": "Discord bot token for authentication" }, "channelId": { "type": "string", "description": "Discord channel ID containing the message" }, "messageId": { "type": "string", "description": "ID of the message to edit (returned by Send Discord Message)" }, "text": { "type": "string", "description": "New message text to replace the existing content" }, "attachmentUrl": { "type": "string", "description": "URL of a file to download and attach to the message (replaces any previous attachments)" } }, "required": ["botToken", "channelId", "messageId", "text"] },
|
|
209
|
+
outputSchema: { "description": "This step does not produce output data." }
|
|
210
|
+
},
|
|
211
|
+
"discordSendFollowUp": {
|
|
212
|
+
stepType: "discordSendFollowUp",
|
|
213
|
+
description: "Send a follow-up message to a Discord slash command interaction.",
|
|
214
|
+
usageNotes: "- Requires the applicationId and interactionToken from the Discord trigger variables.\n- Follow-up messages appear as new messages in the channel after the initial response.\n- Returns the sent message ID.\n- Interaction tokens expire after 15 minutes.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).",
|
|
215
|
+
inputSchema: { "type": "object", "properties": { "applicationId": { "type": "string", "description": "Discord application ID from the bot registration" }, "interactionToken": { "type": "string", "description": "Interaction token provided by the Discord trigger \u2014 expires after 15 minutes" }, "text": { "type": "string", "description": "Message text to send as a follow-up" }, "attachmentUrl": { "type": "string", "description": "URL of a file to download and attach to the message" } }, "required": ["applicationId", "interactionToken", "text"] },
|
|
216
|
+
outputSchema: { "type": "object", "properties": { "messageId": { "type": "string", "description": "ID of the sent follow-up message" } }, "required": ["messageId"] }
|
|
217
|
+
},
|
|
218
|
+
"discordSendMessage": {
|
|
219
|
+
stepType: "discordSendMessage",
|
|
220
|
+
description: "Send a message to Discord \u2014 either edit the loading message or send a new channel message.",
|
|
221
|
+
usageNotes: '- mode "edit" replaces the loading message (interaction response) with the final result. Uses applicationId and interactionToken from trigger variables. No bot permissions required.\n- mode "send" sends a new message to a channel. Uses botToken and channelId from trigger variables. Returns a messageId that can be used with Edit Discord Message.\n- Optionally attach a file by providing a URL to attachmentUrl. The file is downloaded and uploaded to Discord.\n- URLs in the text are automatically embedded by Discord (link previews for images, videos, etc.).\n- Interaction tokens expire after 15 minutes.',
|
|
222
|
+
inputSchema: { "type": "object", "properties": { "mode": { "enum": ["edit", "send"], "type": "string", "description": '"edit" replaces the loading message, "send" sends a new channel message' }, "text": { "type": "string", "description": "Message text to send" }, "applicationId": { "type": "string", "description": 'Discord application ID from the bot registration (required for "reply" mode)' }, "interactionToken": { "type": "string", "description": 'Interaction token provided by the Discord trigger \u2014 expires after 15 minutes (required for "reply" mode)' }, "botToken": { "type": "string", "description": 'Discord bot token for authentication (required for "send" mode)' }, "channelId": { "type": "string", "description": 'Discord channel ID to send the message to (required for "send" mode)' }, "attachmentUrl": { "type": "string", "description": "URL of a file to download and attach to the message" } }, "required": ["mode", "text"] },
|
|
223
|
+
outputSchema: { "type": "object", "properties": { "messageId": { "type": "string", "description": 'ID of the sent Discord message, only present in "send" mode (use with Edit Discord Message)' } } }
|
|
224
|
+
},
|
|
204
225
|
"downloadVideo": {
|
|
205
226
|
stepType: "downloadVideo",
|
|
206
227
|
description: "Download a video file",
|
|
@@ -528,9 +549,13 @@ var init_metadata = __esm({
|
|
|
528
549
|
},
|
|
529
550
|
"logic": {
|
|
530
551
|
stepType: "logic",
|
|
531
|
-
description: "
|
|
532
|
-
usageNotes:
|
|
533
|
-
|
|
552
|
+
description: "Route execution to different branches based on AI evaluation, comparison operators, or workflow jumps.",
|
|
553
|
+
usageNotes: `- Supports two modes: "ai" (default) uses an AI model to pick the most accurate statement; "comparison" uses operator-based checks.
|
|
554
|
+
- In AI mode, the model picks the most accurate statement from the list. All possible cases must be specified.
|
|
555
|
+
- In comparison mode, the context is the left operand and each case's condition is the right operand. First matching case wins. Use operator "default" as a fallback.
|
|
556
|
+
- Requires at least two cases.
|
|
557
|
+
- Each case can transition to a step in the current workflow (destinationStepId) or jump to another workflow (destinationWorkflowId).`,
|
|
558
|
+
inputSchema: { "type": "object", "properties": { "mode": { "enum": ["ai", "comparison"], "type": "string", "description": "Evaluation mode: 'ai' for LLM-based, 'comparison' for operator-based. Default: 'ai'" }, "context": { "type": "string", "description": "AI mode: prompt context. Comparison mode: left operand (resolved via variables)." }, "cases": { "type": "array", "items": { "anyOf": [{ "type": "object", "properties": { "id": { "type": "string", "description": "Unique case identifier" }, "condition": { "type": "string", "description": "AI mode: statement to evaluate. Comparison mode: right operand value." }, "operator": { "enum": ["eq", "neq", "gt", "lt", "gte", "lte", "exists", "not_exists", "contains", "not_contains", "default"], "type": "string", "description": "Comparison operator (comparison mode only)" }, "destinationStepId": { "type": "string", "description": "Step to transition to if this case wins (workflow mode only)" }, "destinationWorkflowId": { "type": "string", "description": "Workflow to jump to if this case wins (uses that workflow's initial step)" } }, "required": ["id", "condition"] }, { "type": "string" }] }, "description": "List of conditions to evaluate (objects for managed UIs, strings for code)" }, "modelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Optional model settings override; uses the organization default if not specified (AI mode only)" } }, "required": ["context", "cases"], "description": "Configuration for the router step" },
|
|
534
559
|
outputSchema: { "type": "object", "properties": { "selectedCase": { "type": "number", "description": "The index of the winning case" } }, "required": ["selectedCase"] }
|
|
535
560
|
},
|
|
536
561
|
"makeDotComRunScenario": {
|
|
@@ -845,9 +870,9 @@ var init_metadata = __esm({
|
|
|
845
870
|
},
|
|
846
871
|
"sendSMS": {
|
|
847
872
|
stepType: "sendSMS",
|
|
848
|
-
description: "Send an SMS
|
|
849
|
-
usageNotes: "- User is responsible for configuring the connection to the number (MindStudio requires double opt-in to prevent spam)",
|
|
850
|
-
inputSchema: { "type": "object", "properties": { "body": { "type": "string", "description": "SMS message body text" }, "connectionId": { "type": "string", "description": "OAuth connection ID for the recipient phone number" } }, "required": ["body"] },
|
|
873
|
+
description: "Send an SMS or MMS message to a phone number configured via OAuth connection.",
|
|
874
|
+
usageNotes: "- User is responsible for configuring the connection to the number (MindStudio requires double opt-in to prevent spam)\n- If mediaUrls are provided, the message is sent as MMS instead of SMS\n- MMS supports up to 10 media URLs (images, video, audio, PDF) with a 5MB limit per file\n- MMS is only supported on US and Canadian carriers; international numbers will receive SMS only (media silently dropped)",
|
|
875
|
+
inputSchema: { "type": "object", "properties": { "body": { "type": "string", "description": "SMS message body text" }, "connectionId": { "type": "string", "description": "OAuth connection ID for the recipient phone number" }, "mediaUrls": { "type": "array", "items": { "type": "string" }, "description": "Optional array of media URLs to send as MMS (up to 10, 5MB each)" } }, "required": ["body"] },
|
|
851
876
|
outputSchema: { "description": "This step does not produce output data." }
|
|
852
877
|
},
|
|
853
878
|
"setRunTitle": {
|
|
@@ -864,6 +889,20 @@ var init_metadata = __esm({
|
|
|
864
889
|
inputSchema: { "type": "object", "properties": { "value": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] } }, "required": ["value"], "description": "Configuration for the set variable step" },
|
|
865
890
|
outputSchema: { "type": "object" }
|
|
866
891
|
},
|
|
892
|
+
"telegramEditMessage": {
|
|
893
|
+
stepType: "telegramEditMessage",
|
|
894
|
+
description: "Edit a previously sent Telegram message. Use with the message ID returned by Send Telegram Message.",
|
|
895
|
+
usageNotes: '- Only text messages sent by the bot can be edited.\n- The messageId is returned by the Send Telegram Message step.\n- Common pattern: send a "Processing..." message, do work, then edit it with the result.',
|
|
896
|
+
inputSchema: { "type": "object", "properties": { "botToken": { "type": "string", "description": 'Telegram bot token in "botId:token" format' }, "chatId": { "type": "string", "description": "Telegram chat ID containing the message" }, "messageId": { "type": "string", "description": "ID of the message to edit" }, "text": { "type": "string", "description": "New message text (MarkdownV2 formatting supported)" } }, "required": ["botToken", "chatId", "messageId", "text"] },
|
|
897
|
+
outputSchema: { "description": "This step does not produce output data." }
|
|
898
|
+
},
|
|
899
|
+
"telegramReplyToMessage": {
|
|
900
|
+
stepType: "telegramReplyToMessage",
|
|
901
|
+
description: "Send a reply to a specific Telegram message. The reply will be visually threaded in the chat.",
|
|
902
|
+
usageNotes: "- Use the rawMessage.message_id from the incoming trigger variables to reply to the user's message.\n- Especially useful in group chats where replies provide context.\n- Returns the sent message ID, which can be used with Edit Telegram Message.",
|
|
903
|
+
inputSchema: { "type": "object", "properties": { "botToken": { "type": "string", "description": 'Telegram bot token in "botId:token" format' }, "chatId": { "type": "string", "description": "Telegram chat ID to send the reply to" }, "replyToMessageId": { "type": "string", "description": "ID of the message to reply to" }, "text": { "type": "string", "description": "Reply text (MarkdownV2 formatting supported)" } }, "required": ["botToken", "chatId", "replyToMessageId", "text"] },
|
|
904
|
+
outputSchema: { "type": "object", "properties": { "messageId": { "type": "number", "description": "ID of the sent reply message" } }, "required": ["messageId"] }
|
|
905
|
+
},
|
|
867
906
|
"telegramSendAudio": {
|
|
868
907
|
stepType: "telegramSendAudio",
|
|
869
908
|
description: "Send an audio file to a Telegram chat as music or a voice note via a bot.",
|
|
@@ -888,9 +927,9 @@ var init_metadata = __esm({
|
|
|
888
927
|
"telegramSendMessage": {
|
|
889
928
|
stepType: "telegramSendMessage",
|
|
890
929
|
description: "Send a text message to a Telegram chat via a bot.",
|
|
891
|
-
usageNotes: '- Messages are sent using MarkdownV2 formatting. Special characters are auto-escaped.\n- botToken format is "botId:token" \u2014 both parts are required.',
|
|
930
|
+
usageNotes: '- Messages are sent using MarkdownV2 formatting. Special characters are auto-escaped.\n- botToken format is "botId:token" \u2014 both parts are required.\n- Returns the sent message ID, which can be used with Edit Telegram Message to update the message later.',
|
|
892
931
|
inputSchema: { "type": "object", "properties": { "botToken": { "type": "string", "description": 'Telegram bot token in "botId:token" format' }, "chatId": { "type": "string", "description": "Telegram chat ID to send the message to" }, "text": { "type": "string", "description": "Message text to send (MarkdownV2 formatting supported)" } }, "required": ["botToken", "chatId", "text"] },
|
|
893
|
-
outputSchema: { "description": "
|
|
932
|
+
outputSchema: { "type": "object", "properties": { "messageId": { "type": "number", "description": "ID of the sent Telegram message" } }, "required": ["messageId"] }
|
|
894
933
|
},
|
|
895
934
|
"telegramSendVideo": {
|
|
896
935
|
stepType: "telegramSendVideo",
|
|
@@ -1159,6 +1198,48 @@ var init_rate_limit = __esm({
|
|
|
1159
1198
|
}
|
|
1160
1199
|
});
|
|
1161
1200
|
|
|
1201
|
+
// src/config.ts
|
|
1202
|
+
var config_exports = {};
|
|
1203
|
+
__export(config_exports, {
|
|
1204
|
+
clearConfig: () => clearConfig,
|
|
1205
|
+
getConfigPath: () => getConfigPath,
|
|
1206
|
+
loadConfig: () => loadConfig,
|
|
1207
|
+
saveConfig: () => saveConfig
|
|
1208
|
+
});
|
|
1209
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
1210
|
+
import { join } from "path";
|
|
1211
|
+
import { homedir } from "os";
|
|
1212
|
+
function getConfigPath() {
|
|
1213
|
+
return CONFIG_PATH;
|
|
1214
|
+
}
|
|
1215
|
+
function loadConfig() {
|
|
1216
|
+
try {
|
|
1217
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
1218
|
+
return JSON.parse(raw);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return {};
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function saveConfig(config) {
|
|
1224
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1225
|
+
writeFileSync(
|
|
1226
|
+
CONFIG_PATH,
|
|
1227
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
1228
|
+
"utf-8"
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
function clearConfig() {
|
|
1232
|
+
saveConfig({});
|
|
1233
|
+
}
|
|
1234
|
+
var CONFIG_DIR, CONFIG_PATH;
|
|
1235
|
+
var init_config = __esm({
|
|
1236
|
+
"src/config.ts"() {
|
|
1237
|
+
"use strict";
|
|
1238
|
+
CONFIG_DIR = join(homedir(), ".mindstudio");
|
|
1239
|
+
CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1162
1243
|
// src/generated/steps.ts
|
|
1163
1244
|
var steps_exports = {};
|
|
1164
1245
|
__export(steps_exports, {
|
|
@@ -1244,6 +1325,15 @@ function applyStepMethods(AgentClass) {
|
|
|
1244
1325
|
proto.detectPII = function(step, options) {
|
|
1245
1326
|
return this.executeStep("detectPII", step, options);
|
|
1246
1327
|
};
|
|
1328
|
+
proto.discordEditMessage = function(step, options) {
|
|
1329
|
+
return this.executeStep("discordEditMessage", step, options);
|
|
1330
|
+
};
|
|
1331
|
+
proto.discordSendFollowUp = function(step, options) {
|
|
1332
|
+
return this.executeStep("discordSendFollowUp", step, options);
|
|
1333
|
+
};
|
|
1334
|
+
proto.discordSendMessage = function(step, options) {
|
|
1335
|
+
return this.executeStep("discordSendMessage", step, options);
|
|
1336
|
+
};
|
|
1247
1337
|
proto.downloadVideo = function(step, options) {
|
|
1248
1338
|
return this.executeStep("downloadVideo", step, options);
|
|
1249
1339
|
};
|
|
@@ -1520,6 +1610,12 @@ function applyStepMethods(AgentClass) {
|
|
|
1520
1610
|
proto.setVariable = function(step, options) {
|
|
1521
1611
|
return this.executeStep("setVariable", step, options);
|
|
1522
1612
|
};
|
|
1613
|
+
proto.telegramEditMessage = function(step, options) {
|
|
1614
|
+
return this.executeStep("telegramEditMessage", step, options);
|
|
1615
|
+
};
|
|
1616
|
+
proto.telegramReplyToMessage = function(step, options) {
|
|
1617
|
+
return this.executeStep("telegramReplyToMessage", step, options);
|
|
1618
|
+
};
|
|
1523
1619
|
proto.telegramSendAudio = function(step, options) {
|
|
1524
1620
|
return this.executeStep("telegramSendAudio", step, options);
|
|
1525
1621
|
};
|
|
@@ -1624,14 +1720,16 @@ __export(client_exports, {
|
|
|
1624
1720
|
function sleep2(ms) {
|
|
1625
1721
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1626
1722
|
}
|
|
1627
|
-
function resolveToken(provided) {
|
|
1723
|
+
function resolveToken(provided, config) {
|
|
1628
1724
|
if (provided) return { token: provided, authType: "apiKey" };
|
|
1629
1725
|
if (process.env.MINDSTUDIO_API_KEY)
|
|
1630
1726
|
return { token: process.env.MINDSTUDIO_API_KEY, authType: "apiKey" };
|
|
1727
|
+
if (config?.apiKey)
|
|
1728
|
+
return { token: config.apiKey, authType: "apiKey" };
|
|
1631
1729
|
if (process.env.CALLBACK_TOKEN)
|
|
1632
1730
|
return { token: process.env.CALLBACK_TOKEN, authType: "internal" };
|
|
1633
1731
|
throw new MindStudioError(
|
|
1634
|
-
"No API key provided.
|
|
1732
|
+
"No API key provided. Run `mindstudio login`, pass `apiKey` to the constructor, or set the MINDSTUDIO_API_KEY environment variable.",
|
|
1635
1733
|
"missing_api_key",
|
|
1636
1734
|
401
|
|
1637
1735
|
);
|
|
@@ -1643,6 +1741,7 @@ var init_client = __esm({
|
|
|
1643
1741
|
init_http();
|
|
1644
1742
|
init_errors();
|
|
1645
1743
|
init_rate_limit();
|
|
1744
|
+
init_config();
|
|
1646
1745
|
init_steps();
|
|
1647
1746
|
init_helpers();
|
|
1648
1747
|
DEFAULT_BASE_URL = "https://v1.mindstudio-api.com";
|
|
@@ -1655,8 +1754,9 @@ var init_client = __esm({
|
|
|
1655
1754
|
/** @internal */
|
|
1656
1755
|
_threadId;
|
|
1657
1756
|
constructor(options = {}) {
|
|
1658
|
-
const
|
|
1659
|
-
const
|
|
1757
|
+
const config = loadConfig();
|
|
1758
|
+
const { token, authType } = resolveToken(options.apiKey, config);
|
|
1759
|
+
const baseUrl = options.baseUrl ?? process.env.MINDSTUDIO_BASE_URL ?? process.env.REMOTE_HOSTNAME ?? config.baseUrl ?? DEFAULT_BASE_URL;
|
|
1660
1760
|
this._reuseThreadId = options.reuseThreadId ?? /^(true|1)$/i.test(process.env.MINDSTUDIO_REUSE_THREAD_ID ?? "");
|
|
1661
1761
|
this._httpConfig = {
|
|
1662
1762
|
baseUrl,
|
|
@@ -1858,7 +1958,7 @@ async function startMcpServer(options) {
|
|
|
1858
1958
|
capabilities: { tools: {} },
|
|
1859
1959
|
serverInfo: {
|
|
1860
1960
|
name: "mindstudio-agent",
|
|
1861
|
-
version: "0.0.
|
|
1961
|
+
version: "0.0.18"
|
|
1862
1962
|
}
|
|
1863
1963
|
});
|
|
1864
1964
|
break;
|
|
@@ -2024,9 +2124,13 @@ var init_mcp = __esm({
|
|
|
2024
2124
|
|
|
2025
2125
|
// src/cli.ts
|
|
2026
2126
|
import { parseArgs } from "util";
|
|
2127
|
+
import { execSync } from "child_process";
|
|
2027
2128
|
var HELP = `Usage: mindstudio <command | method> [options]
|
|
2028
2129
|
|
|
2029
2130
|
Commands:
|
|
2131
|
+
login Authenticate with MindStudio (opens browser)
|
|
2132
|
+
logout Clear stored credentials
|
|
2133
|
+
whoami Show current authentication status
|
|
2030
2134
|
<method> [json | --flags] Execute a step method (shorthand for exec)
|
|
2031
2135
|
exec <method> [json | --flags] Execute a step method
|
|
2032
2136
|
list [--json] List available methods
|
|
@@ -2048,6 +2152,7 @@ Options:
|
|
|
2048
2152
|
--help Show this help
|
|
2049
2153
|
|
|
2050
2154
|
Examples:
|
|
2155
|
+
mindstudio login
|
|
2051
2156
|
mindstudio generate-image --prompt "a sunset"
|
|
2052
2157
|
mindstudio generate-image --prompt "a sunset" --output-key imageUrl
|
|
2053
2158
|
mindstudio generate-text --message "hello" --no-meta
|
|
@@ -2358,6 +2463,300 @@ async function cmdRun(appId, variables, options) {
|
|
|
2358
2463
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2359
2464
|
}
|
|
2360
2465
|
}
|
|
2466
|
+
var ansi = {
|
|
2467
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
2468
|
+
cyanBright: (s) => `\x1B[96m${s}\x1B[0m`,
|
|
2469
|
+
cyanBold: (s) => `\x1B[96;1m${s}\x1B[0m`,
|
|
2470
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
2471
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
2472
|
+
greenBold: (s) => `\x1B[32;1m${s}\x1B[0m`,
|
|
2473
|
+
gray: (s) => `\x1B[90m${s}\x1B[0m`,
|
|
2474
|
+
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
2475
|
+
};
|
|
2476
|
+
var UPDATE_CHECK_INTERVAL = 60 * 60 * 1e3;
|
|
2477
|
+
function isNewerVersion(current, latest) {
|
|
2478
|
+
const c = current.split(".").map(Number);
|
|
2479
|
+
const l = latest.split(".").map(Number);
|
|
2480
|
+
for (let i = 0; i < Math.max(c.length, l.length); i++) {
|
|
2481
|
+
const cv = c[i] ?? 0;
|
|
2482
|
+
const lv = l[i] ?? 0;
|
|
2483
|
+
if (lv > cv) return true;
|
|
2484
|
+
if (lv < cv) return false;
|
|
2485
|
+
}
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2488
|
+
async function checkForUpdate() {
|
|
2489
|
+
const currentVersion = "0.0.18";
|
|
2490
|
+
if (!currentVersion) return null;
|
|
2491
|
+
try {
|
|
2492
|
+
const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2493
|
+
const config = loadConfig2();
|
|
2494
|
+
if (config._updateCheck) {
|
|
2495
|
+
const age = Date.now() - config._updateCheck.checkedAt;
|
|
2496
|
+
if (age < UPDATE_CHECK_INTERVAL) {
|
|
2497
|
+
return isNewerVersion(currentVersion, config._updateCheck.latestVersion) ? config._updateCheck.latestVersion : null;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
const res = await fetch(
|
|
2501
|
+
"https://registry.npmjs.org/@mindstudio-ai/agent/latest",
|
|
2502
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
2503
|
+
);
|
|
2504
|
+
if (!res.ok) return null;
|
|
2505
|
+
const data = await res.json();
|
|
2506
|
+
const latestVersion = data.version;
|
|
2507
|
+
if (!latestVersion) return null;
|
|
2508
|
+
saveConfig2({
|
|
2509
|
+
...config,
|
|
2510
|
+
_updateCheck: { latestVersion, checkedAt: Date.now() }
|
|
2511
|
+
});
|
|
2512
|
+
return isNewerVersion(currentVersion, latestVersion) ? latestVersion : null;
|
|
2513
|
+
} catch {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function printUpdateNotice(latestVersion) {
|
|
2518
|
+
const currentVersion = "0.0.18";
|
|
2519
|
+
process.stderr.write(
|
|
2520
|
+
`
|
|
2521
|
+
${ansi.cyanBright("Update available")} ${ansi.gray(currentVersion + " \u2192")} ${ansi.cyanBold(latestVersion)}
|
|
2522
|
+
${ansi.gray("Run")} npm install -g @mindstudio-ai/agent ${ansi.gray("to update")}
|
|
2523
|
+
`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
var LOGO = ` .=+-. :++.
|
|
2527
|
+
*@@@@@+ :%@@@@%:
|
|
2528
|
+
.%@@@@@@#..@@@@@@@=
|
|
2529
|
+
.*@@@@@@@--@@@@@@@#.**.
|
|
2530
|
+
*@@@@@@@.-@@@@@@@@.#@@*
|
|
2531
|
+
.#@@@@@@@-.@@@@@@@* #@@@@%.
|
|
2532
|
+
=@@@@@@@-.@@@@@@@#.-@@@@@@+
|
|
2533
|
+
:@@@@@@: +@@@@@#. .@@@@@@:
|
|
2534
|
+
.++: .-*-. .++:`;
|
|
2535
|
+
function printLogo() {
|
|
2536
|
+
const lines = LOGO.split("\n");
|
|
2537
|
+
for (const line of lines) {
|
|
2538
|
+
const colored = line.replace(
|
|
2539
|
+
/[^\s]/g,
|
|
2540
|
+
(ch) => ch === "." || ch === ":" || ch === "-" || ch === "+" || ch === "=" ? `\x1B[36m${ch}\x1B[0m` : `\x1B[96;1m${ch}\x1B[0m`
|
|
2541
|
+
);
|
|
2542
|
+
process.stderr.write(` ${colored}
|
|
2543
|
+
`);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function openBrowser(url) {
|
|
2547
|
+
try {
|
|
2548
|
+
if (process.platform === "darwin") execSync(`open "${url}"`);
|
|
2549
|
+
else if (process.platform === "win32") execSync(`start "" "${url}"`);
|
|
2550
|
+
else execSync(`xdg-open "${url}"`);
|
|
2551
|
+
} catch {
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function sleep3(ms) {
|
|
2555
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2556
|
+
}
|
|
2557
|
+
function waitForKeypress() {
|
|
2558
|
+
return new Promise((resolve) => {
|
|
2559
|
+
if (!process.stdin.isTTY) {
|
|
2560
|
+
resolve();
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
process.stdin.setRawMode(true);
|
|
2564
|
+
process.stdin.resume();
|
|
2565
|
+
process.stdin.once("data", () => {
|
|
2566
|
+
process.stdin.setRawMode(false);
|
|
2567
|
+
process.stdin.pause();
|
|
2568
|
+
resolve();
|
|
2569
|
+
});
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
function maskKey(key) {
|
|
2573
|
+
if (key.length <= 8) return "****";
|
|
2574
|
+
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
2575
|
+
}
|
|
2576
|
+
var DEFAULT_BASE_URL2 = "https://v1.mindstudio-api.com";
|
|
2577
|
+
var SPINNER_FRAMES = [
|
|
2578
|
+
"\u28FE",
|
|
2579
|
+
"\u28FD",
|
|
2580
|
+
"\u28FB",
|
|
2581
|
+
"\u28BF",
|
|
2582
|
+
"\u287F",
|
|
2583
|
+
"\u28DF",
|
|
2584
|
+
"\u28EF",
|
|
2585
|
+
"\u28F7"
|
|
2586
|
+
];
|
|
2587
|
+
async function cmdLogin(options) {
|
|
2588
|
+
const baseUrl = options.baseUrl ?? process.env.MINDSTUDIO_BASE_URL ?? process.env.REMOTE_HOSTNAME ?? DEFAULT_BASE_URL2;
|
|
2589
|
+
process.stderr.write("\n");
|
|
2590
|
+
printLogo();
|
|
2591
|
+
process.stderr.write("\n");
|
|
2592
|
+
const ver = "0.0.18";
|
|
2593
|
+
process.stderr.write(
|
|
2594
|
+
` ${ansi.bold("MindStudio")} ${ansi.gray("CLI")}${ver ? " " + ansi.gray("v" + ver) : ""}
|
|
2595
|
+
`
|
|
2596
|
+
);
|
|
2597
|
+
process.stderr.write(
|
|
2598
|
+
` ${ansi.gray("Connect your MindStudio account to get started.")}
|
|
2599
|
+
|
|
2600
|
+
`
|
|
2601
|
+
);
|
|
2602
|
+
process.stderr.write(
|
|
2603
|
+
` ${ansi.cyanBright("Press any key to open the browser...")}
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
|
|
2607
|
+
`
|
|
2608
|
+
);
|
|
2609
|
+
await waitForKeypress();
|
|
2610
|
+
process.stderr.write("\x1B[4A\r\x1B[J");
|
|
2611
|
+
process.stderr.write(
|
|
2612
|
+
` ${ansi.gray("Requesting authorization...")}
|
|
2613
|
+
`
|
|
2614
|
+
);
|
|
2615
|
+
const authRes = await fetch(
|
|
2616
|
+
`${baseUrl}/developer/v2/request-auth-url`,
|
|
2617
|
+
{
|
|
2618
|
+
headers: {
|
|
2619
|
+
"Content-Type": "application/json",
|
|
2620
|
+
"User-Agent": "@mindstudio-ai/agent"
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
);
|
|
2624
|
+
if (!authRes.ok) {
|
|
2625
|
+
fatal(
|
|
2626
|
+
`Failed to request auth URL: ${authRes.status} ${authRes.statusText}`
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
const { url, token } = await authRes.json();
|
|
2630
|
+
openBrowser(url);
|
|
2631
|
+
process.stderr.write(
|
|
2632
|
+
` ${ansi.cyanBright("Opening browser to authenticate...")}
|
|
2633
|
+
|
|
2634
|
+
${ansi.gray("If the browser didn't open, visit:")}
|
|
2635
|
+
${ansi.cyan(url)}
|
|
2636
|
+
|
|
2637
|
+
`
|
|
2638
|
+
);
|
|
2639
|
+
const POLL_INTERVAL = 2e3;
|
|
2640
|
+
const MAX_ATTEMPTS = 60;
|
|
2641
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
2642
|
+
await sleep3(POLL_INTERVAL);
|
|
2643
|
+
const frame = SPINNER_FRAMES[attempt % SPINNER_FRAMES.length];
|
|
2644
|
+
const remaining = Math.ceil(
|
|
2645
|
+
MAX_ATTEMPTS * POLL_INTERVAL / 1e3 - (attempt + 1) * POLL_INTERVAL / 1e3
|
|
2646
|
+
);
|
|
2647
|
+
process.stderr.write(
|
|
2648
|
+
`\r ${ansi.cyan(frame)} Waiting for browser authorization... ${ansi.gray(`(${remaining}s)`)}`
|
|
2649
|
+
);
|
|
2650
|
+
const pollRes = await fetch(`${baseUrl}/developer/v2/poll-auth-url`, {
|
|
2651
|
+
method: "POST",
|
|
2652
|
+
headers: {
|
|
2653
|
+
"Content-Type": "application/json",
|
|
2654
|
+
"User-Agent": "@mindstudio-ai/agent"
|
|
2655
|
+
},
|
|
2656
|
+
body: JSON.stringify({ token })
|
|
2657
|
+
});
|
|
2658
|
+
if (!pollRes.ok) {
|
|
2659
|
+
process.stderr.write("\n");
|
|
2660
|
+
fatal(
|
|
2661
|
+
`Poll request failed: ${pollRes.status} ${pollRes.statusText}`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
const result = await pollRes.json();
|
|
2665
|
+
if (result.status === "completed" && result.apiKey) {
|
|
2666
|
+
process.stderr.write("\r\x1B[K");
|
|
2667
|
+
const { saveConfig: saveConfig2, getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2668
|
+
const config = {
|
|
2669
|
+
apiKey: result.apiKey
|
|
2670
|
+
};
|
|
2671
|
+
if (baseUrl !== DEFAULT_BASE_URL2) {
|
|
2672
|
+
config.baseUrl = baseUrl;
|
|
2673
|
+
}
|
|
2674
|
+
saveConfig2(config);
|
|
2675
|
+
process.stderr.write(
|
|
2676
|
+
` ${ansi.greenBold("\u2714")} Authenticated successfully!
|
|
2677
|
+
${ansi.gray("Credentials saved to")} ${getConfigPath2()}
|
|
2678
|
+
|
|
2679
|
+
`
|
|
2680
|
+
);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (result.status === "expired") {
|
|
2684
|
+
process.stderr.write("\r\x1B[K");
|
|
2685
|
+
fatal("Authorization expired. Please try again.");
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
process.stderr.write("\r\x1B[K");
|
|
2689
|
+
fatal("Authorization timed out. Please try again.");
|
|
2690
|
+
}
|
|
2691
|
+
async function cmdLogout() {
|
|
2692
|
+
const { loadConfig: loadConfig2, clearConfig: clearConfig2, getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2693
|
+
const config = loadConfig2();
|
|
2694
|
+
if (!config.apiKey) {
|
|
2695
|
+
process.stderr.write(
|
|
2696
|
+
` ${ansi.gray("Not currently logged in.")}
|
|
2697
|
+
`
|
|
2698
|
+
);
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
clearConfig2();
|
|
2702
|
+
process.stderr.write(
|
|
2703
|
+
` ${ansi.greenBold("\u2714")} Logged out. Credentials removed from ${ansi.gray(getConfigPath2())}
|
|
2704
|
+
`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
async function cmdWhoami(options) {
|
|
2708
|
+
let source;
|
|
2709
|
+
let detail = [];
|
|
2710
|
+
if (options.apiKey) {
|
|
2711
|
+
source = `${ansi.bold("--api-key flag")} ${ansi.gray("(CLI argument)")}`;
|
|
2712
|
+
} else if (process.env.MINDSTUDIO_API_KEY) {
|
|
2713
|
+
source = `${ansi.bold("MINDSTUDIO_API_KEY")} ${ansi.gray("(environment variable)")}`;
|
|
2714
|
+
detail.push(
|
|
2715
|
+
` ${ansi.gray("Key:")} ${maskKey(process.env.MINDSTUDIO_API_KEY)}`
|
|
2716
|
+
);
|
|
2717
|
+
} else {
|
|
2718
|
+
const { loadConfig: loadConfig2, getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2719
|
+
const config = loadConfig2();
|
|
2720
|
+
if (config.apiKey) {
|
|
2721
|
+
source = `${ansi.bold("config file")} ${ansi.gray("(mindstudio login)")}`;
|
|
2722
|
+
detail.push(` ${ansi.gray("File:")} ${getConfigPath2()}`);
|
|
2723
|
+
detail.push(` ${ansi.gray("Key:")} ${maskKey(config.apiKey)}`);
|
|
2724
|
+
if (config.baseUrl) {
|
|
2725
|
+
detail.push(` ${ansi.gray("URL:")} ${config.baseUrl}`);
|
|
2726
|
+
}
|
|
2727
|
+
} else if (process.env.CALLBACK_TOKEN) {
|
|
2728
|
+
source = `${ansi.bold("CALLBACK_TOKEN")} ${ansi.gray("(managed/internal mode)")}`;
|
|
2729
|
+
} else {
|
|
2730
|
+
process.stderr.write(
|
|
2731
|
+
` ${ansi.gray("\u25CB")} Not authenticated. Run ${ansi.cyan("mindstudio login")} to get started.
|
|
2732
|
+
`
|
|
2733
|
+
);
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
process.stderr.write(` ${ansi.gray("Auth:")} ${source}
|
|
2738
|
+
`);
|
|
2739
|
+
for (const line of detail) process.stderr.write(line + "\n");
|
|
2740
|
+
process.stderr.write(` ${ansi.gray("Verifying...")} `);
|
|
2741
|
+
try {
|
|
2742
|
+
const { MindStudioAgent: MindStudioAgent2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2743
|
+
const agent = new MindStudioAgent2({
|
|
2744
|
+
apiKey: options.apiKey,
|
|
2745
|
+
baseUrl: options.baseUrl
|
|
2746
|
+
});
|
|
2747
|
+
const result = await agent.listAgents();
|
|
2748
|
+
process.stderr.write(
|
|
2749
|
+
`\r\x1B[K ${ansi.greenBold("\u25CF")} ${ansi.green("Connected")} ${ansi.gray("\u2014")} ${result.orgName} ${ansi.gray("(" + result.orgId + ")")}
|
|
2750
|
+
`
|
|
2751
|
+
);
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2754
|
+
process.stderr.write(
|
|
2755
|
+
`\r\x1B[K ${ansi.dim("\u25CF")} ${ansi.dim("Not connected")} ${ansi.gray("\u2014")} ${message}
|
|
2756
|
+
`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2361
2760
|
function parseStepFlags(argv) {
|
|
2362
2761
|
const result = {};
|
|
2363
2762
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2431,7 +2830,25 @@ async function main() {
|
|
|
2431
2830
|
process.exit(positionals.length === 0 ? 1 : 0);
|
|
2432
2831
|
}
|
|
2433
2832
|
const command = positionals[0];
|
|
2833
|
+
const updatePromise = command !== "mcp" && command !== "login" ? checkForUpdate() : Promise.resolve(null);
|
|
2434
2834
|
try {
|
|
2835
|
+
if (command === "login") {
|
|
2836
|
+
await cmdLogin({
|
|
2837
|
+
baseUrl: values["base-url"]
|
|
2838
|
+
});
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
if (command === "logout") {
|
|
2842
|
+
await cmdLogout();
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (command === "whoami") {
|
|
2846
|
+
await cmdWhoami({
|
|
2847
|
+
apiKey: values["api-key"],
|
|
2848
|
+
baseUrl: values["base-url"]
|
|
2849
|
+
});
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2435
2852
|
if (command === "list") {
|
|
2436
2853
|
await cmdList(values.json);
|
|
2437
2854
|
return;
|
|
@@ -2551,6 +2968,9 @@ async function main() {
|
|
|
2551
2968
|
} catch (err) {
|
|
2552
2969
|
const message = err instanceof Error ? err.message : String(err);
|
|
2553
2970
|
fatal(message);
|
|
2971
|
+
} finally {
|
|
2972
|
+
const latestVersion = await updatePromise;
|
|
2973
|
+
if (latestVersion) printUpdateNotice(latestVersion);
|
|
2554
2974
|
}
|
|
2555
2975
|
}
|
|
2556
2976
|
main();
|