@nodes/agent 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/core/providers.d.ts +9 -1
- package/dist/core/providers.js +21 -1
- package/dist/core/serve.js +119 -26
- package/package.json +24 -16
- package/src/cli.ts +120 -0
- package/src/index.ts +3 -0
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/core/providers.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a language model from provider + model ID.
|
|
4
|
+
*
|
|
5
|
+
* Supports two formats:
|
|
6
|
+
* 1. Explicit provider: resolveModel('minimax', 'MiniMax-M2.7')
|
|
7
|
+
* 2. Slash format: resolveModel('minimax/MiniMax-M2.7')
|
|
8
|
+
* (provider extracted from prefix, rest is model ID)
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveModel(provider: string, modelId?: string): LanguageModel;
|
package/dist/core/providers.js
CHANGED
|
@@ -2,7 +2,25 @@ import { createGateway } from '@ai-sdk/gateway';
|
|
|
2
2
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
3
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
4
4
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
5
|
+
import { createMinimax } from 'vercel-minimax-ai-provider';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a language model from provider + model ID.
|
|
8
|
+
*
|
|
9
|
+
* Supports two formats:
|
|
10
|
+
* 1. Explicit provider: resolveModel('minimax', 'MiniMax-M2.7')
|
|
11
|
+
* 2. Slash format: resolveModel('minimax/MiniMax-M2.7')
|
|
12
|
+
* (provider extracted from prefix, rest is model ID)
|
|
13
|
+
*/
|
|
5
14
|
export function resolveModel(provider, modelId) {
|
|
15
|
+
// Support "provider/model" format — split on first slash
|
|
16
|
+
if (!modelId && provider.includes('/')) {
|
|
17
|
+
const idx = provider.indexOf('/');
|
|
18
|
+
modelId = provider.slice(idx + 1);
|
|
19
|
+
provider = provider.slice(0, idx);
|
|
20
|
+
}
|
|
21
|
+
if (!modelId) {
|
|
22
|
+
throw new Error(`No model ID provided for provider "${provider}"`);
|
|
23
|
+
}
|
|
6
24
|
switch (provider) {
|
|
7
25
|
case 'openai':
|
|
8
26
|
return createOpenAI({ apiKey: process.env.OPENAI_API_KEY }).languageModel(modelId);
|
|
@@ -10,8 +28,10 @@ export function resolveModel(provider, modelId) {
|
|
|
10
28
|
return createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY }).languageModel(modelId);
|
|
11
29
|
case 'google':
|
|
12
30
|
return createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY }).languageModel(modelId);
|
|
31
|
+
case 'minimax':
|
|
32
|
+
return createMinimax({ apiKey: process.env.MINIMAX_API_KEY }).languageModel(modelId);
|
|
13
33
|
case 'gateway':
|
|
14
34
|
default:
|
|
15
|
-
return createGateway({ apiKey: process.env.GATEWAY_API_KEY }).languageModel(modelId);
|
|
35
|
+
return createGateway({ apiKey: process.env.GATEWAY_API_KEY }).languageModel(modelId.includes('/') ? modelId : `${provider}/${modelId}`);
|
|
16
36
|
}
|
|
17
37
|
}
|
package/dist/core/serve.js
CHANGED
|
@@ -5,7 +5,9 @@ import { createServer } from 'node:http';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { execBash, execReadFile, execWriteFile } from './tools.js';
|
|
7
7
|
import { createCLIStream, isCLIProvider } from './cli-stream.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveModel } from './providers.js';
|
|
9
|
+
import { localTools } from './tools.js';
|
|
10
|
+
import { JsonToSseTransformStream, readUIMessageStream, streamText, hasToolCall, stepCountIs } from 'ai';
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
12
|
const pkg = require('../../package.json');
|
|
11
13
|
export async function serve(options = {}) {
|
|
@@ -128,27 +130,118 @@ export async function serve(options = {}) {
|
|
|
128
130
|
res.end(JSON.stringify({ error: 'Missing prompt or messages' }));
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
|
-
const
|
|
132
|
-
if (!isCLIProvider(provider)) {
|
|
133
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
134
|
-
res.end(JSON.stringify({ error: `Unsupported CLI provider: ${provider}` }));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
133
|
+
const rawProvider = (body.provider || 'claude-cli');
|
|
137
134
|
const startTime = Date.now();
|
|
138
|
-
const
|
|
139
|
-
console.log(`[chat] → Received: provider=${
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
135
|
+
const streamMode = body.stream === false ? 'off' : body.stream === 'raw' ? 'raw' : 'sse';
|
|
136
|
+
console.log(`[chat] → Received: provider=${rawProvider}, model=${body.model || 'default'}, stream=${streamMode}, prompt=${prompt.length} chars`);
|
|
137
|
+
// Resolve execution stream — CLI binary or API provider
|
|
138
|
+
let execStream;
|
|
139
|
+
if (isCLIProvider(rawProvider)) {
|
|
140
|
+
// CLI providers: spawn local binary
|
|
141
|
+
console.log(`[chat] Spawning CLI stream...`);
|
|
142
|
+
execStream = createCLIStream({
|
|
143
|
+
provider: rawProvider,
|
|
144
|
+
prompt,
|
|
145
|
+
systemPrompt: body.systemPrompt,
|
|
146
|
+
model: body.model,
|
|
147
|
+
bypassPermissions: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// API providers (including nodes-cli): resolve model from body.model (e.g. "minimax/MiniMax-M2.7")
|
|
152
|
+
const modelId = body.model || rawProvider;
|
|
153
|
+
console.log(`[chat] Resolving API model: ${modelId}`);
|
|
154
|
+
try {
|
|
155
|
+
const model = resolveModel(modelId);
|
|
156
|
+
const startAt = Date.now();
|
|
157
|
+
let firstTokenAt;
|
|
158
|
+
const result = streamText({
|
|
159
|
+
model,
|
|
160
|
+
system: body.systemPrompt,
|
|
161
|
+
tools: localTools,
|
|
162
|
+
stopWhen: [hasToolCall('ai_end'), stepCountIs(50)],
|
|
163
|
+
...(body.messages?.length
|
|
164
|
+
? { messages: body.messages }
|
|
165
|
+
: { prompt }),
|
|
166
|
+
});
|
|
167
|
+
// Parse provider/model from the slash format for metadata
|
|
168
|
+
const providerName = modelId.includes('/') ? modelId.split('/')[0] : rawProvider;
|
|
169
|
+
const modelName = modelId.includes('/') ? modelId.split('/').slice(1).join('/') : modelId;
|
|
170
|
+
// Embed real metadata into the stream — same pattern as handlerV2's createMessageMetadata
|
|
171
|
+
const messageMetadata = ({ part }) => {
|
|
172
|
+
if (!firstTokenAt && part && (part.type === 'text-delta' || part.type === 'reasoning-delta')) {
|
|
173
|
+
firstTokenAt = Date.now();
|
|
174
|
+
return { timings: { ttfbMs: firstTokenAt - startAt } };
|
|
175
|
+
}
|
|
176
|
+
if (part.type === 'start') {
|
|
177
|
+
return { provider: providerName, model: modelName };
|
|
178
|
+
}
|
|
179
|
+
if (part.type === 'finish') {
|
|
180
|
+
const totalMs = Date.now() - startAt;
|
|
181
|
+
const outputPhaseMs = firstTokenAt ? Date.now() - firstTokenAt : undefined;
|
|
182
|
+
const usageRaw = (part.totalUsage ?? {});
|
|
183
|
+
const outputTokenDetails = usageRaw.outputTokenDetails;
|
|
184
|
+
const inputTokenDetails = usageRaw.inputTokenDetails;
|
|
185
|
+
const outputTokens = typeof usageRaw.outputTokens === 'number' ? usageRaw.outputTokens : 0;
|
|
186
|
+
const reasoningTokens = outputTokenDetails?.reasoningTokens ?? (typeof usageRaw.reasoningTokens === 'number' ? usageRaw.reasoningTokens : 0);
|
|
187
|
+
const cachedInputTokens = inputTokenDetails?.cacheReadTokens;
|
|
188
|
+
const tokensPerSec = (outputTokens + reasoningTokens) > 0 && outputPhaseMs
|
|
189
|
+
? (outputTokens + reasoningTokens) / (outputPhaseMs / 1000) : undefined;
|
|
190
|
+
return {
|
|
191
|
+
totalUsage: {
|
|
192
|
+
...usageRaw,
|
|
193
|
+
reasoningTokens: reasoningTokens || undefined,
|
|
194
|
+
cachedInputTokens: cachedInputTokens || undefined,
|
|
195
|
+
},
|
|
196
|
+
timings: { ttfbMs: firstTokenAt ? firstTokenAt - startAt : undefined, totalMs, outputPhaseMs, tokensPerSec },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
};
|
|
201
|
+
execStream = result.toUIMessageStream({ messageMetadata, sendReasoning: true });
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.error('[chat] ✗ Model resolution failed:', err);
|
|
205
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
206
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to resolve model' }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (streamMode === 'raw') {
|
|
211
|
+
// Raw UIMessageStream chunks — for server-to-server (Nodes web)
|
|
212
|
+
res.writeHead(200, {
|
|
213
|
+
'Content-Type': 'application/x-ndjson',
|
|
214
|
+
'Cache-Control': 'no-cache',
|
|
215
|
+
Connection: 'keep-alive',
|
|
216
|
+
'X-Accel-Buffering': 'no',
|
|
217
|
+
});
|
|
218
|
+
res.flushHeaders();
|
|
219
|
+
console.log(`[chat] Streaming raw...`);
|
|
220
|
+
let chunkCount = 0;
|
|
221
|
+
const reader = execStream.getReader();
|
|
222
|
+
try {
|
|
223
|
+
while (true) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done)
|
|
226
|
+
break;
|
|
227
|
+
chunkCount++;
|
|
228
|
+
// Each chunk is a JSON object from the UIMessageStream — write as NDJSON
|
|
229
|
+
const line = typeof value === 'string' ? value : JSON.stringify(value);
|
|
230
|
+
res.write(line + '\n');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error('[chat] ✗ Stream error:', err);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
238
|
+
console.log(`[chat] ← Done (raw): ${chunkCount} chunks in ${elapsed}s`);
|
|
239
|
+
res.end();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (streamMode === 'sse') {
|
|
243
|
+
// SSE streaming response — for browsers/external clients
|
|
244
|
+
const sseStream = execStream.pipeThrough(new JsonToSseTransformStream());
|
|
152
245
|
res.writeHead(200, {
|
|
153
246
|
'Content-Type': 'text/event-stream',
|
|
154
247
|
'Cache-Control': 'no-cache',
|
|
@@ -157,7 +250,7 @@ export async function serve(options = {}) {
|
|
|
157
250
|
'X-Vercel-AI-UI-Message-Stream': 'v1',
|
|
158
251
|
});
|
|
159
252
|
res.flushHeaders();
|
|
160
|
-
console.log(`[chat] Streaming
|
|
253
|
+
console.log(`[chat] Streaming SSE...`);
|
|
161
254
|
let chunkCount = 0;
|
|
162
255
|
const reader = sseStream.getReader();
|
|
163
256
|
try {
|
|
@@ -177,15 +270,15 @@ export async function serve(options = {}) {
|
|
|
177
270
|
}
|
|
178
271
|
finally {
|
|
179
272
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
180
|
-
console.log(`[chat] ← Done: ${chunkCount} chunks in ${elapsed}s`);
|
|
273
|
+
console.log(`[chat] ← Done (SSE): ${chunkCount} chunks in ${elapsed}s`);
|
|
181
274
|
res.end();
|
|
182
275
|
}
|
|
183
276
|
}
|
|
184
277
|
else {
|
|
185
|
-
// Non-stream: consume
|
|
278
|
+
// Non-stream: consume stream, return JSON with final UIMessage
|
|
186
279
|
try {
|
|
187
280
|
let lastAssistant;
|
|
188
|
-
for await (const snapshot of readUIMessageStream({ stream:
|
|
281
|
+
for await (const snapshot of readUIMessageStream({ stream: execStream })) {
|
|
189
282
|
if (snapshot.role === 'assistant')
|
|
190
283
|
lastAssistant = snapshot;
|
|
191
284
|
}
|
|
@@ -198,7 +291,7 @@ export async function serve(options = {}) {
|
|
|
198
291
|
catch (err) {
|
|
199
292
|
console.error('[chat] ✗ Error:', err);
|
|
200
293
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
201
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : '
|
|
294
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Execution failed' }));
|
|
202
295
|
}
|
|
203
296
|
}
|
|
204
297
|
return;
|
package/package.json
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodes/agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Autonomous AI agent runtime for Nodes",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"nodes": "
|
|
9
|
+
"nodes": "src/cli.ts"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"nodes": "dist/cli.js"
|
|
16
|
+
}
|
|
10
17
|
},
|
|
11
18
|
"files": [
|
|
12
19
|
"dist"
|
|
13
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"dev": "tsx --env-file=.env src/cli.ts",
|
|
24
|
+
"prompt": "tsx --env-file=.env src/cli.ts -p",
|
|
25
|
+
"serve": "tsx --env-file=.env src/cli.ts --serve",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"test:integration": "vitest run src/tests/run-once.test.ts"
|
|
29
|
+
},
|
|
14
30
|
"dependencies": {
|
|
15
31
|
"@ai-sdk/anthropic": "3.0.47",
|
|
16
32
|
"@ai-sdk/gateway": "3.0.46",
|
|
@@ -18,24 +34,16 @@
|
|
|
18
34
|
"@ai-sdk/openai": "3.0.33",
|
|
19
35
|
"@mariozechner/pi-tui": "^0.55.0",
|
|
20
36
|
"@modelcontextprotocol/sdk": "1.23.0",
|
|
37
|
+
"@nodes/sdk": "workspace:*",
|
|
21
38
|
"ai": "6.0.86",
|
|
22
39
|
"picocolors": "^1.1.1",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
40
|
+
"vercel-minimax-ai-provider": "0.0.2",
|
|
41
|
+
"zod": "3.25.76"
|
|
25
42
|
},
|
|
26
43
|
"devDependencies": {
|
|
27
44
|
"@types/node": "^24.0.0",
|
|
28
45
|
"tsx": "^4.19.0",
|
|
29
46
|
"typescript": "^5.6.0",
|
|
30
47
|
"vitest": "^4.0.18"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "tsc",
|
|
34
|
-
"dev": "tsx --env-file=.env src/cli.ts",
|
|
35
|
-
"prompt": "tsx --env-file=.env src/cli.ts -p",
|
|
36
|
-
"serve": "tsx --env-file=.env src/cli.ts --serve",
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"test:watch": "vitest",
|
|
39
|
-
"test:integration": "vitest run src/tests/run-once.test.ts"
|
|
40
48
|
}
|
|
41
|
-
}
|
|
49
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'node:util'
|
|
4
|
+
import { createRequire } from 'module'
|
|
5
|
+
import { NodesClient } from '@nodes/sdk'
|
|
6
|
+
import { runOnce, interactive } from './core/loop.js'
|
|
7
|
+
import { interactiveTUI } from './core/tui-chat.js'
|
|
8
|
+
import { serve } from './core/serve.js'
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url)
|
|
11
|
+
const pkg = require('../package.json') as { version: string }
|
|
12
|
+
|
|
13
|
+
const { values } = parseArgs({
|
|
14
|
+
options: {
|
|
15
|
+
prompt: { type: 'string', short: 'p' },
|
|
16
|
+
node: { type: 'string', short: 'n' },
|
|
17
|
+
provider: { type: 'string' },
|
|
18
|
+
serve: { type: 'boolean' },
|
|
19
|
+
simple: { type: 'boolean' },
|
|
20
|
+
help: { type: 'boolean', short: 'h' },
|
|
21
|
+
version: { type: 'boolean', short: 'v' },
|
|
22
|
+
},
|
|
23
|
+
strict: false,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (values.help) {
|
|
27
|
+
console.log(`
|
|
28
|
+
nodes — Autonomous AI agent for Nodes
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
nodes Start in interactive mode (new chat)
|
|
32
|
+
nodes -n <nodeId> Resume interactive chat on existing node
|
|
33
|
+
nodes -p "prompt" Run a single prompt and exit
|
|
34
|
+
nodes -p "prompt" -n <id> Run prompt in existing chat and exit
|
|
35
|
+
nodes --provider claude-cli Use local Claude CLI (Max subscription)
|
|
36
|
+
nodes --serve Start MCP server (exposes local tools)
|
|
37
|
+
nodes --simple Use simple readline UI (no TUI)
|
|
38
|
+
nodes --help Show this help
|
|
39
|
+
|
|
40
|
+
Environment:
|
|
41
|
+
NODES_URL Nodes instance URL (default: https://nodes.ws)
|
|
42
|
+
NODES_API_KEY API key for authentication
|
|
43
|
+
NODES_AGENT Agent slug (e.g. "nodes/my-agent") or user ID
|
|
44
|
+
PORT MCP server port (default: 8788, --serve only)
|
|
45
|
+
`)
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (values.version) {
|
|
50
|
+
console.log(`nodes ${pkg.version}`)
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --serve doesn't require Nodes credentials (purely local MCP server)
|
|
55
|
+
if (values.serve) {
|
|
56
|
+
serve().catch((err) => {
|
|
57
|
+
console.error(err.message)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
// All other modes require Nodes connection
|
|
62
|
+
const url = process.env.NODES_URL || 'https://nodes.ws'
|
|
63
|
+
const apiKey = process.env.NODES_API_KEY || ''
|
|
64
|
+
const agent = process.env.NODES_AGENT || ''
|
|
65
|
+
|
|
66
|
+
if (!apiKey) {
|
|
67
|
+
console.error('Error: NODES_API_KEY is required.')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!agent) {
|
|
72
|
+
console.error('Error: NODES_AGENT is required (agent slug or user ID).')
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract userId from user-scoped token (nodes_<userId>_<secret>)
|
|
77
|
+
const tokenMatch = apiKey.match(/^nodes_([a-f0-9]{24})_[a-f0-9]{32}$/)
|
|
78
|
+
const userId = tokenMatch?.[1]
|
|
79
|
+
|
|
80
|
+
const prompt = values.prompt as string | undefined
|
|
81
|
+
const nodeId = values.node as string | undefined
|
|
82
|
+
const provider = values.provider as string | undefined
|
|
83
|
+
|
|
84
|
+
const nodes = new NodesClient({ url, apiKey, agent })
|
|
85
|
+
const config = { nodes, agent, userId, prompt, nodeId, provider, nodesUrl: url, nodesApiKey: apiKey }
|
|
86
|
+
|
|
87
|
+
if (prompt) {
|
|
88
|
+
// One-shot mode
|
|
89
|
+
runOnce(config)
|
|
90
|
+
.then((result) => {
|
|
91
|
+
console.log(JSON.stringify(result, null, 2))
|
|
92
|
+
process.exit(0)
|
|
93
|
+
})
|
|
94
|
+
.catch((err) => {
|
|
95
|
+
console.error(err.message)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
})
|
|
98
|
+
.finally(() => nodes.disconnect())
|
|
99
|
+
} else {
|
|
100
|
+
if (values.simple) {
|
|
101
|
+
// Simple readline mode
|
|
102
|
+
process.on('SIGINT', async () => {
|
|
103
|
+
console.log('\nGoodbye.')
|
|
104
|
+
await nodes.disconnect()
|
|
105
|
+
process.exit(0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
interactive(config).catch((err) => {
|
|
109
|
+
console.error(err.message)
|
|
110
|
+
process.exit(1)
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
// TUI mode (default) — handles its own SIGINT
|
|
114
|
+
interactiveTUI(config).catch((err) => {
|
|
115
|
+
console.error(err.message)
|
|
116
|
+
process.exit(1)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.ts
ADDED