@genspark/cli 1.0.3 → 1.0.5
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 +282 -24
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +179 -27
- package/dist/client.js.map +1 -1
- package/dist/index.js +468 -136
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +12 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,32 +18,34 @@
|
|
|
18
18
|
* Tool name: "web_search" -> Command: `gsk web_search`, Alias: `gsk search`
|
|
19
19
|
* Tool name: "crawler" -> Command: `gsk crawler`, Alias: `gsk crawl`
|
|
20
20
|
*/
|
|
21
|
-
import { Command } from
|
|
22
|
-
import * as fs from
|
|
23
|
-
import * as
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
import { Command } from 'commander';
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as http from 'http';
|
|
24
|
+
import * as https from 'https';
|
|
25
|
+
import * as pathModule from 'path';
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
import { ApiClient, unauthenticatedRequest } from './client.js';
|
|
28
|
+
import { setDebugEnabled, setOutputFormat, debug, info, error as logError, output, } from './logger.js';
|
|
29
|
+
import { loadConfigFile, saveConfigFile, getConfigPath, loadToolsCache, saveToolsCache, } from './config.js';
|
|
30
|
+
import { checkForUpdates } from './updater.js';
|
|
31
|
+
const VERSION = '1.1.0';
|
|
32
|
+
const DEFAULT_BASE_URL = 'https://www.genspark.ai';
|
|
31
33
|
const DEFAULT_TIMEOUT = 300000; // 5 minutes (video generation can be slow)
|
|
32
34
|
// Load config file (lowest priority)
|
|
33
35
|
const fileConfig = loadConfigFile();
|
|
34
36
|
// Create main program
|
|
35
37
|
const program = new Command();
|
|
36
38
|
program
|
|
37
|
-
.name(
|
|
38
|
-
.description(
|
|
39
|
+
.name('gsk')
|
|
40
|
+
.description('Genspark Tool CLI - Search, crawl, analyze, and generate media')
|
|
39
41
|
.version(VERSION)
|
|
40
|
-
.option(
|
|
41
|
-
.option(
|
|
42
|
-
.option(
|
|
43
|
-
.option(
|
|
44
|
-
.option(
|
|
45
|
-
.option(
|
|
46
|
-
.option(
|
|
42
|
+
.option('--api-key <key>', 'API key (or set GSK_API_KEY env var)')
|
|
43
|
+
.option('--base-url <url>', `Base URL for API (default: ${DEFAULT_BASE_URL})`, DEFAULT_BASE_URL)
|
|
44
|
+
.option('--project-id <id>', 'Project ID for access control (or set GSK_PROJECT_ID env var)')
|
|
45
|
+
.option('--debug', 'Enable debug output and request more details from server', false)
|
|
46
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', String(DEFAULT_TIMEOUT))
|
|
47
|
+
.option('--output <format>', 'Output format: json or text', 'json')
|
|
48
|
+
.option('--refresh', 'Force refresh of cached tool schemas from server');
|
|
47
49
|
/**
|
|
48
50
|
* Get global options with priority:
|
|
49
51
|
* 1. Command-line options (highest)
|
|
@@ -81,75 +83,193 @@ function createClient() {
|
|
|
81
83
|
setDebugEnabled(options.debug);
|
|
82
84
|
setOutputFormat(options.output);
|
|
83
85
|
if (!options.apiKey) {
|
|
84
|
-
logError(
|
|
85
|
-
logError(
|
|
86
|
-
logError(
|
|
86
|
+
logError('API key is required. Provide it via:');
|
|
87
|
+
logError(' 1. --api-key option');
|
|
88
|
+
logError(' 2. GSK_API_KEY environment variable');
|
|
87
89
|
logError(` 3. Config file: ${getConfigPath()}`);
|
|
88
90
|
process.exit(1);
|
|
89
91
|
}
|
|
90
92
|
debug(`Base URL: ${options.baseUrl}`);
|
|
91
93
|
debug(`Timeout: ${options.timeout}ms`);
|
|
92
94
|
debug(`Debug mode: ${options.debug}`);
|
|
93
|
-
debug(`Project ID: ${options.projectId ||
|
|
95
|
+
debug(`Project ID: ${options.projectId || '(not set)'}`);
|
|
94
96
|
return new ApiClient(options);
|
|
95
97
|
}
|
|
96
98
|
// ============================================
|
|
97
99
|
// Shared helpers for local file handling
|
|
98
100
|
// ============================================
|
|
99
101
|
const CONTENT_TYPE_MAP = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
102
|
+
'.jpg': 'image/jpeg',
|
|
103
|
+
'.jpeg': 'image/jpeg',
|
|
104
|
+
'.png': 'image/png',
|
|
105
|
+
'.gif': 'image/gif',
|
|
106
|
+
'.webp': 'image/webp',
|
|
107
|
+
'.pdf': 'application/pdf',
|
|
108
|
+
'.txt': 'text/plain',
|
|
109
|
+
'.json': 'application/json',
|
|
110
|
+
'.html': 'text/html',
|
|
111
|
+
'.css': 'text/css',
|
|
112
|
+
'.js': 'application/javascript',
|
|
113
|
+
'.ts': 'application/typescript',
|
|
114
|
+
'.md': 'text/markdown',
|
|
115
|
+
'.xml': 'application/xml',
|
|
116
|
+
'.csv': 'text/csv',
|
|
117
|
+
'.zip': 'application/zip',
|
|
118
|
+
'.mp3': 'audio/mpeg',
|
|
119
|
+
'.mp4': 'video/mp4',
|
|
120
|
+
'.wav': 'audio/wav',
|
|
121
|
+
'.webm': 'video/webm',
|
|
120
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Stream a local file to a URL using Node.js http/https.request +
|
|
125
|
+
* fs.createReadStream so the file is never fully buffered in memory.
|
|
126
|
+
* Suitable for files of any size.
|
|
127
|
+
*
|
|
128
|
+
* Reports upload progress to stderr for files larger than PROGRESS_THRESHOLD.
|
|
129
|
+
* Returns the response body as a string (empty for typical blob-storage PUTs).
|
|
130
|
+
*/
|
|
131
|
+
async function streamFileUpload(filePath, uploadUrl, headers, fileSize, method = 'PUT') {
|
|
132
|
+
const PROGRESS_THRESHOLD = 5 * 1024 * 1024; // 5 MB
|
|
133
|
+
const showProgress = fileSize > PROGRESS_THRESHOLD;
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const parsedUrl = new URL(uploadUrl);
|
|
136
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
137
|
+
const transport = isHttps ? https : http;
|
|
138
|
+
// Declare before transport.request so closures below don't reference it
|
|
139
|
+
// in the temporal dead zone.
|
|
140
|
+
let progressTimer = null;
|
|
141
|
+
const req = transport.request({
|
|
142
|
+
hostname: parsedUrl.hostname,
|
|
143
|
+
port: parsedUrl.port
|
|
144
|
+
? parseInt(parsedUrl.port, 10)
|
|
145
|
+
: isHttps
|
|
146
|
+
? 443
|
|
147
|
+
: 80,
|
|
148
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
149
|
+
method,
|
|
150
|
+
headers: { ...headers, 'Content-Length': String(fileSize) },
|
|
151
|
+
}, res => {
|
|
152
|
+
// Always collect the full response body before resolving/rejecting
|
|
153
|
+
let body = '';
|
|
154
|
+
res.on('data', (chunk) => {
|
|
155
|
+
body += chunk.toString();
|
|
156
|
+
});
|
|
157
|
+
res.on('end', () => {
|
|
158
|
+
if (progressTimer)
|
|
159
|
+
clearInterval(progressTimer);
|
|
160
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
161
|
+
resolve(body);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Stop the file stream and close the request on non-2xx so we
|
|
165
|
+
// don't keep piping data into a request the server has rejected.
|
|
166
|
+
fileStream.destroy();
|
|
167
|
+
req.destroy();
|
|
168
|
+
reject(new Error(`Upload failed: HTTP ${res.statusCode}${body ? ': ' + body.substring(0, 200) : ''}`));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
res.on('error', (err) => {
|
|
172
|
+
if (progressTimer)
|
|
173
|
+
clearInterval(progressTimer);
|
|
174
|
+
reject(err);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
const fileStream = fs.createReadStream(filePath);
|
|
178
|
+
req.on('error', (err) => {
|
|
179
|
+
if (progressTimer)
|
|
180
|
+
clearInterval(progressTimer);
|
|
181
|
+
fileStream.destroy(); // prevent the readable from continuing to push data
|
|
182
|
+
reject(err);
|
|
183
|
+
});
|
|
184
|
+
let uploadedBytes = 0;
|
|
185
|
+
if (showProgress) {
|
|
186
|
+
const totalMB = (fileSize / (1024 * 1024)).toFixed(1);
|
|
187
|
+
progressTimer = setInterval(() => {
|
|
188
|
+
const pct = Math.floor((uploadedBytes / fileSize) * 100);
|
|
189
|
+
const doneMB = (uploadedBytes / (1024 * 1024)).toFixed(1);
|
|
190
|
+
process.stderr.write(`\r[INFO] Uploading... ${pct}% ${doneMB} / ${totalMB} MB `);
|
|
191
|
+
}, 1000);
|
|
192
|
+
}
|
|
193
|
+
fileStream.on('data', (chunk) => {
|
|
194
|
+
uploadedBytes +=
|
|
195
|
+
typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
|
196
|
+
});
|
|
197
|
+
fileStream.on('end', () => {
|
|
198
|
+
if (progressTimer) {
|
|
199
|
+
clearInterval(progressTimer);
|
|
200
|
+
process.stderr.write('\r\x1b[K'); // clear the progress line
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
fileStream.on('error', (err) => {
|
|
204
|
+
if (progressTimer)
|
|
205
|
+
clearInterval(progressTimer);
|
|
206
|
+
req.destroy(); // close the HTTP request so the TCP connection is not leaked
|
|
207
|
+
reject(err);
|
|
208
|
+
});
|
|
209
|
+
fileStream.pipe(req);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function formatFileSize(bytes) {
|
|
213
|
+
if (bytes >= 1024 * 1024)
|
|
214
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
215
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
216
|
+
}
|
|
121
217
|
async function uploadLocalFile(filePath, client) {
|
|
122
218
|
const fileName = pathModule.basename(filePath);
|
|
123
219
|
const ext = pathModule.extname(filePath).toLowerCase();
|
|
124
|
-
const contentType = CONTENT_TYPE_MAP[ext] ||
|
|
125
|
-
const
|
|
126
|
-
info(`Uploading ${fileName} (${(
|
|
220
|
+
const contentType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
|
|
221
|
+
const fileSize = fs.statSync(filePath).size;
|
|
222
|
+
info(`Uploading ${fileName} (${formatFileSize(fileSize)}, ${contentType})...`);
|
|
127
223
|
const globalOpts = getGlobalOptions();
|
|
128
224
|
const uploadUrlResult = await client.getUploadUrl({
|
|
129
225
|
content_type: contentType,
|
|
130
226
|
name: fileName,
|
|
131
227
|
project_id: globalOpts.projectId,
|
|
132
228
|
});
|
|
133
|
-
if (uploadUrlResult.status !==
|
|
229
|
+
if (uploadUrlResult.status !== 'ok' || !uploadUrlResult.data) {
|
|
134
230
|
throw new Error(`Failed to get upload URL: ${uploadUrlResult.message}`);
|
|
135
231
|
}
|
|
136
232
|
const { upload_url, file_wrapper_url } = uploadUrlResult.data;
|
|
137
233
|
debug(`Upload URL: ${upload_url.substring(0, 80)}...`);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
method: "PUT",
|
|
141
|
-
headers: { "Content-Type": contentType, "x-ms-blob-type": "BlockBlob" },
|
|
142
|
-
body: fileBuffer,
|
|
143
|
-
});
|
|
144
|
-
if (!uploadResponse.ok)
|
|
145
|
-
throw new Error(`Upload failed: HTTP ${uploadResponse.status}`);
|
|
234
|
+
// Stream the file directly to Azure Blob Storage — no full-file buffering
|
|
235
|
+
await streamFileUpload(filePath, upload_url, { 'Content-Type': contentType, 'x-ms-blob-type': 'BlockBlob' }, fileSize);
|
|
146
236
|
info(`Uploaded → ${file_wrapper_url}`);
|
|
147
237
|
return file_wrapper_url;
|
|
148
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Upload a local file directly to AI Drive using the dedicated tool_cli
|
|
241
|
+
* endpoint (POST /api/tool_cli/aidrive/upload). The file is streamed without
|
|
242
|
+
* buffering the whole content in memory, so large files (100 MB+) work fine.
|
|
243
|
+
*
|
|
244
|
+
* This avoids the roundabout blob-upload + download_file two-step approach.
|
|
245
|
+
*/
|
|
246
|
+
async function uploadToAiDrive(filePath, uploadPath, override = false) {
|
|
247
|
+
const fileName = pathModule.basename(filePath);
|
|
248
|
+
const ext = pathModule.extname(filePath).toLowerCase();
|
|
249
|
+
const contentType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
|
|
250
|
+
const fileSize = fs.statSync(filePath).size;
|
|
251
|
+
info(`Uploading ${fileName} (${formatFileSize(fileSize)}, ${contentType}) directly to AI Drive...`);
|
|
252
|
+
const globalOpts = getGlobalOptions();
|
|
253
|
+
const baseUrl = globalOpts.baseUrl.replace(/\/$/, '');
|
|
254
|
+
const uploadUrl = `${baseUrl}/api/tool_cli/aidrive/upload?upload_path=${encodeURIComponent(uploadPath)}${override ? '&override=true' : ''}`;
|
|
255
|
+
const headers = { 'Content-Type': contentType };
|
|
256
|
+
if (globalOpts.apiKey)
|
|
257
|
+
headers['X-Api-Key'] = globalOpts.apiKey;
|
|
258
|
+
if (globalOpts.projectId)
|
|
259
|
+
headers['X-Project-ID'] = globalOpts.projectId;
|
|
260
|
+
debug(`AI Drive upload URL: ${uploadUrl}`);
|
|
261
|
+
const responseBody = await streamFileUpload(filePath, uploadUrl, headers, fileSize, 'POST');
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(responseBody);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new Error(`Unexpected response from AI Drive upload: ${responseBody.substring(0, 200)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
149
269
|
async function downloadToFile(wrapperUrl, savePath, client) {
|
|
150
270
|
info(`Downloading to ${savePath}...`);
|
|
151
271
|
const result = await client.getDownloadUrl({ file_wrapper_url: wrapperUrl });
|
|
152
|
-
if (result.status !==
|
|
272
|
+
if (result.status !== 'ok' || !result.data)
|
|
153
273
|
throw new Error(`Failed to get download URL: ${result.message}`);
|
|
154
274
|
const resp = await fetch(result.data.download_url);
|
|
155
275
|
if (!resp.ok)
|
|
@@ -160,9 +280,9 @@ async function downloadToFile(wrapperUrl, savePath, client) {
|
|
|
160
280
|
return pathModule.resolve(savePath);
|
|
161
281
|
}
|
|
162
282
|
function isLocalFilePath(value) {
|
|
163
|
-
return (!value.startsWith(
|
|
164
|
-
!value.startsWith(
|
|
165
|
-
!value.startsWith(
|
|
283
|
+
return (!value.startsWith('http://') &&
|
|
284
|
+
!value.startsWith('https://') &&
|
|
285
|
+
!value.startsWith('/api/') &&
|
|
166
286
|
fs.existsSync(value));
|
|
167
287
|
}
|
|
168
288
|
async function resolveFileArg(value, client) {
|
|
@@ -179,10 +299,10 @@ async function resolveLocalFiles(args, client) {
|
|
|
179
299
|
const resolved = { ...args };
|
|
180
300
|
for (const [key, value] of Object.entries(resolved)) {
|
|
181
301
|
// Handle array of URLs (e.g., image_urls)
|
|
182
|
-
if (key.endsWith(
|
|
302
|
+
if (key.endsWith('_urls') && Array.isArray(value)) {
|
|
183
303
|
const resolvedUrls = [];
|
|
184
304
|
for (const item of value) {
|
|
185
|
-
if (typeof item ===
|
|
305
|
+
if (typeof item === 'string') {
|
|
186
306
|
resolvedUrls.push(await resolveFileArg(item, client));
|
|
187
307
|
}
|
|
188
308
|
else {
|
|
@@ -192,8 +312,8 @@ async function resolveLocalFiles(args, client) {
|
|
|
192
312
|
resolved[key] = resolvedUrls;
|
|
193
313
|
}
|
|
194
314
|
// Handle single URL (e.g., url, audio_url)
|
|
195
|
-
else if ((key ===
|
|
196
|
-
typeof value ===
|
|
315
|
+
else if ((key === 'url' || key.endsWith('_url')) &&
|
|
316
|
+
typeof value === 'string') {
|
|
197
317
|
resolved[key] = await resolveFileArg(value, client);
|
|
198
318
|
}
|
|
199
319
|
}
|
|
@@ -201,6 +321,9 @@ async function resolveLocalFiles(args, client) {
|
|
|
201
321
|
}
|
|
202
322
|
// ============================================
|
|
203
323
|
// Dynamic tool command registration
|
|
324
|
+
// Map of group name -> parent Command for hierarchical subcommands
|
|
325
|
+
// (e.g., "email" -> Command for `gsk email [subcommand]`)
|
|
326
|
+
const _groupCommands = new Map();
|
|
204
327
|
// ============================================
|
|
205
328
|
/**
|
|
206
329
|
* Extract a file URL from nested result data using dot-separated path keys.
|
|
@@ -209,14 +332,14 @@ async function resolveLocalFiles(args, client) {
|
|
|
209
332
|
function extractFileUrl(data, keys) {
|
|
210
333
|
for (const key of keys) {
|
|
211
334
|
let current = data;
|
|
212
|
-
for (const part of key.split(
|
|
213
|
-
if (current == null || typeof current !==
|
|
335
|
+
for (const part of key.split('.')) {
|
|
336
|
+
if (current == null || typeof current !== 'object') {
|
|
214
337
|
current = undefined;
|
|
215
338
|
break;
|
|
216
339
|
}
|
|
217
340
|
current = current[part];
|
|
218
341
|
}
|
|
219
|
-
if (typeof current ===
|
|
342
|
+
if (typeof current === 'string' && current.length > 0)
|
|
220
343
|
return current;
|
|
221
344
|
}
|
|
222
345
|
return null;
|
|
@@ -225,7 +348,27 @@ function extractFileUrl(data, keys) {
|
|
|
225
348
|
* Register a dynamic tool command from a server-provided schema
|
|
226
349
|
*/
|
|
227
350
|
function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
228
|
-
|
|
351
|
+
// If the tool has a group, register as a subcommand under the group parent
|
|
352
|
+
// e.g., tool.name="email_list" with cli.group="email" creates `gsk email list`
|
|
353
|
+
let targetProgram = parentProgram;
|
|
354
|
+
let cmdName = tool.name;
|
|
355
|
+
if (tool.cli.group) {
|
|
356
|
+
const groupName = tool.cli.group;
|
|
357
|
+
if (!_groupCommands.has(groupName)) {
|
|
358
|
+
const groupCmd = parentProgram
|
|
359
|
+
.command(groupName)
|
|
360
|
+
.description(`Commands for ${groupName}`);
|
|
361
|
+
_groupCommands.set(groupName, groupCmd);
|
|
362
|
+
}
|
|
363
|
+
targetProgram = _groupCommands.get(groupName);
|
|
364
|
+
// Derive subcommand name: strip group prefix from tool name
|
|
365
|
+
// e.g., "email_list" with group "email" -> "list"
|
|
366
|
+
const prefix = groupName + '_';
|
|
367
|
+
cmdName = tool.name.startsWith(prefix)
|
|
368
|
+
? tool.name.slice(prefix.length)
|
|
369
|
+
: tool.name;
|
|
370
|
+
}
|
|
371
|
+
const cmd = targetProgram.command(cmdName).description(tool.description);
|
|
229
372
|
// Register aliases
|
|
230
373
|
for (const alias of tool.cli.aliases) {
|
|
231
374
|
cmd.alias(alias);
|
|
@@ -236,7 +379,7 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
236
379
|
if (tool.cli.primary_arg && props[tool.cli.primary_arg]) {
|
|
237
380
|
const param = props[tool.cli.primary_arg];
|
|
238
381
|
const isRequired = required.includes(tool.cli.primary_arg);
|
|
239
|
-
const bracket = isRequired ?
|
|
382
|
+
const bracket = isRequired ? '<value>' : '[value]';
|
|
240
383
|
cmd.argument(bracket, param.description || tool.cli.primary_arg);
|
|
241
384
|
}
|
|
242
385
|
// Add remaining params as --options
|
|
@@ -247,8 +390,8 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
247
390
|
const isRequired = required.includes(name);
|
|
248
391
|
const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
|
|
249
392
|
const shortAlias = aliases[name];
|
|
250
|
-
const shortPrefix = shortAlias ? `-${shortAlias}, ` :
|
|
251
|
-
if (paramType ===
|
|
393
|
+
const shortPrefix = shortAlias ? `-${shortAlias}, ` : '';
|
|
394
|
+
if (paramType === 'array') {
|
|
252
395
|
const flag = `${shortPrefix}--${name} <values...>`;
|
|
253
396
|
if (isRequired) {
|
|
254
397
|
cmd.requiredOption(flag, param.description || name);
|
|
@@ -257,7 +400,7 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
257
400
|
cmd.option(flag, param.description || name);
|
|
258
401
|
}
|
|
259
402
|
}
|
|
260
|
-
else if (paramType ===
|
|
403
|
+
else if (paramType === 'boolean') {
|
|
261
404
|
cmd.option(`${shortPrefix}--${name}`, param.description || name);
|
|
262
405
|
}
|
|
263
406
|
else {
|
|
@@ -273,13 +416,18 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
273
416
|
// Add -o/--output-file option for tools that produce downloadable files
|
|
274
417
|
const outputFileKeys = tool.cli.output_file_keys || [];
|
|
275
418
|
if (outputFileKeys.length > 0) {
|
|
276
|
-
cmd.option(
|
|
419
|
+
cmd.option('-o, --output-file <path>', 'Download the generated file to a local path');
|
|
420
|
+
}
|
|
421
|
+
// For the aidrive tool, add client-side options used with --local_file upload
|
|
422
|
+
if (tool.name === 'aidrive') {
|
|
423
|
+
cmd.option('--local_file <path>', 'Local file path to upload to AI Drive (supports files >5 MB; alternative to --file_content)');
|
|
424
|
+
cmd.option('--override', 'Overwrite an existing file at the destination path (used with --local_file)', false);
|
|
277
425
|
}
|
|
278
426
|
// Generic action handler
|
|
279
427
|
cmd.action(async (primaryArgValue, opts) => {
|
|
280
428
|
// Commander passes opts as second arg when there's an argument,
|
|
281
429
|
// or as first arg when there's no argument
|
|
282
|
-
if (typeof primaryArgValue ===
|
|
430
|
+
if (typeof primaryArgValue === 'object' && primaryArgValue !== null) {
|
|
283
431
|
opts = primaryArgValue;
|
|
284
432
|
primaryArgValue = undefined;
|
|
285
433
|
}
|
|
@@ -299,30 +447,62 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
299
447
|
const paramType = Array.isArray(param.type)
|
|
300
448
|
? param.type[0]
|
|
301
449
|
: param.type;
|
|
302
|
-
if (paramType ===
|
|
450
|
+
if (paramType === 'integer' || paramType === 'number') {
|
|
303
451
|
const num = Number(args[name]);
|
|
304
452
|
if (!isNaN(num)) {
|
|
305
|
-
args[name] = paramType ===
|
|
453
|
+
args[name] = paramType === 'integer' ? Math.floor(num) : num;
|
|
306
454
|
}
|
|
307
455
|
}
|
|
308
456
|
}
|
|
309
457
|
}
|
|
458
|
+
// aidrive: strip client-side-only options before sending to API.
|
|
459
|
+
// --local_file and --override are only meaningful to the CLI and must
|
|
460
|
+
// never be forwarded to the server (other aidrive actions like ls,
|
|
461
|
+
// mkdir, move don't know about them).
|
|
462
|
+
if (tool.name === 'aidrive') {
|
|
463
|
+
delete args.local_file;
|
|
464
|
+
delete args.override;
|
|
465
|
+
}
|
|
466
|
+
// aidrive: handle --local_file for the upload action.
|
|
467
|
+
// Stream the file directly to AI Drive via the dedicated tool_cli
|
|
468
|
+
// endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
|
|
469
|
+
if (tool.name === 'aidrive' &&
|
|
470
|
+
primaryArgValue === 'upload' &&
|
|
471
|
+
opts.local_file) {
|
|
472
|
+
const localFilePath = opts.local_file;
|
|
473
|
+
const override = !!opts.override;
|
|
474
|
+
if (!fs.existsSync(localFilePath)) {
|
|
475
|
+
logError(`File not found: ${localFilePath}`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
// args.upload_path is safe here: args = { ...opts } at line above, and
|
|
479
|
+
// only local_file/override were deleted from args — upload_path is intact.
|
|
480
|
+
const rawUploadPath = args.upload_path ||
|
|
481
|
+
`/${pathModule.basename(localFilePath)}`;
|
|
482
|
+
const uploadPath = rawUploadPath.startsWith('/')
|
|
483
|
+
? rawUploadPath
|
|
484
|
+
: `/${rawUploadPath}`;
|
|
485
|
+
// Stream directly to AI Drive — single request, no blob middleman
|
|
486
|
+
const result = await uploadToAiDrive(localFilePath, uploadPath, override);
|
|
487
|
+
output(result);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
310
490
|
// Resolve local file paths to uploaded URLs
|
|
311
491
|
const resolvedArgs = await resolveLocalFiles(args, client);
|
|
312
492
|
const result = await client.executeTool(tool.name, resolvedArgs);
|
|
313
493
|
// If -o was specified and tool succeeded, download the output file
|
|
314
494
|
if (outputFilePath &&
|
|
315
495
|
outputFileKeys.length > 0 &&
|
|
316
|
-
result.status ===
|
|
496
|
+
result.status === 'ok' &&
|
|
317
497
|
result.data &&
|
|
318
|
-
typeof result.data ===
|
|
498
|
+
typeof result.data === 'object') {
|
|
319
499
|
const fileUrl = extractFileUrl(result.data, outputFileKeys);
|
|
320
500
|
if (fileUrl) {
|
|
321
501
|
const localPath = await downloadToFile(fileUrl, outputFilePath, client);
|
|
322
502
|
result.data.local_path = localPath;
|
|
323
503
|
}
|
|
324
504
|
else {
|
|
325
|
-
info(
|
|
505
|
+
info('Warning: Could not find a file URL in the result to download');
|
|
326
506
|
}
|
|
327
507
|
}
|
|
328
508
|
output(result);
|
|
@@ -338,9 +518,9 @@ function registerToolCommand(parentProgram, tool, clientFactory) {
|
|
|
338
518
|
// ============================================
|
|
339
519
|
// List tools
|
|
340
520
|
program
|
|
341
|
-
.command(
|
|
342
|
-
.alias(
|
|
343
|
-
.description(
|
|
521
|
+
.command('list-tools')
|
|
522
|
+
.alias('ls')
|
|
523
|
+
.description('List all available tools')
|
|
344
524
|
.action(async () => {
|
|
345
525
|
try {
|
|
346
526
|
const client = createClient();
|
|
@@ -354,9 +534,9 @@ program
|
|
|
354
534
|
});
|
|
355
535
|
// Upload file - get upload URL, upload file, return file wrapper URL
|
|
356
536
|
program
|
|
357
|
-
.command(
|
|
358
|
-
.description(
|
|
359
|
-
.argument(
|
|
537
|
+
.command('upload')
|
|
538
|
+
.description('Upload a local file and get a file wrapper URL')
|
|
539
|
+
.argument('<file>', 'Local file path to upload')
|
|
360
540
|
.action(async (filePath) => {
|
|
361
541
|
try {
|
|
362
542
|
if (!fs.existsSync(filePath)) {
|
|
@@ -367,11 +547,11 @@ program
|
|
|
367
547
|
const file_wrapper_url = await uploadLocalFile(filePath, client);
|
|
368
548
|
const fileName = pathModule.basename(filePath);
|
|
369
549
|
const ext = pathModule.extname(filePath).toLowerCase();
|
|
370
|
-
const contentType = CONTENT_TYPE_MAP[ext] ||
|
|
550
|
+
const contentType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
|
|
371
551
|
const stats = fs.statSync(filePath);
|
|
372
552
|
output({
|
|
373
|
-
status:
|
|
374
|
-
message:
|
|
553
|
+
status: 'ok',
|
|
554
|
+
message: 'File uploaded successfully',
|
|
375
555
|
data: {
|
|
376
556
|
file_wrapper_url,
|
|
377
557
|
file_name: fileName,
|
|
@@ -387,17 +567,17 @@ program
|
|
|
387
567
|
});
|
|
388
568
|
// Download file - get download URL from file wrapper URL
|
|
389
569
|
program
|
|
390
|
-
.command(
|
|
391
|
-
.description(
|
|
392
|
-
.argument(
|
|
393
|
-
.option(
|
|
570
|
+
.command('download')
|
|
571
|
+
.description('Get a download URL for a file wrapper URL')
|
|
572
|
+
.argument('<file_wrapper_url>', 'File wrapper URL (e.g., /api/files/s/xxxxx)')
|
|
573
|
+
.option('-s, --save <path>', 'Download and save to local file path')
|
|
394
574
|
.action(async (fileWrapperUrl, opts) => {
|
|
395
575
|
try {
|
|
396
576
|
const client = createClient();
|
|
397
577
|
const result = await client.getDownloadUrl({
|
|
398
578
|
file_wrapper_url: fileWrapperUrl,
|
|
399
579
|
});
|
|
400
|
-
if (result.status !==
|
|
580
|
+
if (result.status !== 'ok' || !result.data) {
|
|
401
581
|
logError(`Failed to get download URL: ${result.message}`);
|
|
402
582
|
process.exit(1);
|
|
403
583
|
}
|
|
@@ -412,8 +592,8 @@ program
|
|
|
412
592
|
const localPath = pathModule.resolve(opts.save);
|
|
413
593
|
info(`Downloaded ${(buffer.length / 1024).toFixed(1)} KB → ${localPath}`);
|
|
414
594
|
output({
|
|
415
|
-
status:
|
|
416
|
-
message:
|
|
595
|
+
status: 'ok',
|
|
596
|
+
message: 'File downloaded successfully',
|
|
417
597
|
data: {
|
|
418
598
|
...result.data,
|
|
419
599
|
local_path: localPath,
|
|
@@ -433,8 +613,8 @@ program
|
|
|
433
613
|
});
|
|
434
614
|
// Login - browser-based device flow authentication
|
|
435
615
|
program
|
|
436
|
-
.command(
|
|
437
|
-
.description(
|
|
616
|
+
.command('login')
|
|
617
|
+
.description('Log in to Genspark via browser to obtain an API key')
|
|
438
618
|
.action(async () => {
|
|
439
619
|
const globalOpts = getGlobalOptions();
|
|
440
620
|
setDebugEnabled(globalOpts.debug);
|
|
@@ -442,78 +622,78 @@ program
|
|
|
442
622
|
const baseUrl = globalOpts.baseUrl;
|
|
443
623
|
try {
|
|
444
624
|
// Step 1: Request device code
|
|
445
|
-
info(
|
|
446
|
-
const deviceResp = await unauthenticatedRequest(baseUrl,
|
|
625
|
+
info('Requesting device code...');
|
|
626
|
+
const deviceResp = await unauthenticatedRequest(baseUrl, '/api/cli_auth/device_code', 'POST');
|
|
447
627
|
const { device_code, auth_url, poll_interval, expires_in } = deviceResp;
|
|
448
628
|
// Step 2: Open browser (use spawn to avoid shell command injection)
|
|
449
|
-
info(
|
|
629
|
+
info('Opening browser for login...');
|
|
450
630
|
info(`Login URL: ${auth_url}`);
|
|
451
631
|
// Validate auth_url is a same-origin HTTP(S) URL before passing to spawn
|
|
452
632
|
try {
|
|
453
633
|
const parsed = new URL(auth_url);
|
|
454
634
|
const base = new URL(baseUrl);
|
|
455
|
-
if (parsed.protocol !==
|
|
456
|
-
throw new Error(
|
|
635
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
636
|
+
throw new Error('auth_url is not http(s)');
|
|
457
637
|
}
|
|
458
638
|
if (parsed.host !== base.host) {
|
|
459
|
-
throw new Error(
|
|
639
|
+
throw new Error('auth_url host does not match baseUrl host');
|
|
460
640
|
}
|
|
461
641
|
let cmd;
|
|
462
642
|
let args;
|
|
463
|
-
if (process.platform ===
|
|
643
|
+
if (process.platform === 'win32') {
|
|
464
644
|
// Avoid cmd.exe which interprets & in URLs as command separators
|
|
465
|
-
cmd =
|
|
466
|
-
args = [
|
|
645
|
+
cmd = 'rundll32';
|
|
646
|
+
args = ['url.dll,FileProtocolHandler', auth_url];
|
|
467
647
|
}
|
|
468
|
-
else if (process.platform ===
|
|
469
|
-
cmd =
|
|
648
|
+
else if (process.platform === 'darwin') {
|
|
649
|
+
cmd = 'open';
|
|
470
650
|
args = [auth_url];
|
|
471
651
|
}
|
|
472
652
|
else {
|
|
473
|
-
cmd =
|
|
653
|
+
cmd = 'xdg-open';
|
|
474
654
|
args = [auth_url];
|
|
475
655
|
}
|
|
476
|
-
const child = spawn(cmd, args, { stdio:
|
|
477
|
-
child.on(
|
|
478
|
-
info(
|
|
479
|
-
info(
|
|
656
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
657
|
+
child.on('error', () => {
|
|
658
|
+
info('Could not open browser automatically.');
|
|
659
|
+
info('Please open the URL above manually.');
|
|
480
660
|
});
|
|
481
661
|
child.unref();
|
|
482
662
|
}
|
|
483
663
|
catch {
|
|
484
|
-
info(
|
|
485
|
-
info(
|
|
664
|
+
info('Could not open browser automatically.');
|
|
665
|
+
info('Please open the URL above manually.');
|
|
486
666
|
}
|
|
487
667
|
info(`Waiting for authorization (expires in ${expires_in}s, press Ctrl+C to cancel)...`);
|
|
488
668
|
// Step 3: Poll for token
|
|
489
669
|
const startTime = Date.now();
|
|
490
670
|
const timeoutMs = expires_in * 1000;
|
|
491
671
|
while (Date.now() - startTime < timeoutMs) {
|
|
492
|
-
await new Promise(
|
|
672
|
+
await new Promise(resolve => setTimeout(resolve, poll_interval * 1000));
|
|
493
673
|
const elapsedSec = Math.round((Date.now() - startTime) / 1000);
|
|
494
674
|
const remainingSec = expires_in - elapsedSec;
|
|
495
|
-
const tokenResp = await unauthenticatedRequest(baseUrl, `/api/cli_auth/token?code=${encodeURIComponent(device_code)}`,
|
|
496
|
-
if (tokenResp.status ===
|
|
675
|
+
const tokenResp = await unauthenticatedRequest(baseUrl, `/api/cli_auth/token?code=${encodeURIComponent(device_code)}`, 'GET');
|
|
676
|
+
if (tokenResp.status === 'approved' && tokenResp.api_key) {
|
|
497
677
|
// Save to config
|
|
498
678
|
const config = loadConfigFile();
|
|
499
679
|
config.api_key = tokenResp.api_key;
|
|
500
680
|
saveConfigFile(config);
|
|
501
|
-
info(
|
|
681
|
+
info('Login successful! API key saved.');
|
|
502
682
|
output({
|
|
503
|
-
status:
|
|
504
|
-
message:
|
|
683
|
+
status: 'ok',
|
|
684
|
+
message: 'Login successful',
|
|
505
685
|
data: { config_path: getConfigPath() },
|
|
506
686
|
});
|
|
507
687
|
return;
|
|
508
688
|
}
|
|
509
|
-
if (tokenResp.status ===
|
|
510
|
-
logError(
|
|
689
|
+
if (tokenResp.status === 'expired') {
|
|
690
|
+
logError('Authorization expired. Please try again.');
|
|
511
691
|
process.exit(1);
|
|
512
692
|
}
|
|
513
693
|
// status === "pending" — keep polling with countdown
|
|
514
694
|
info(`Still waiting for authorization... (${remainingSec}s remaining)`);
|
|
515
695
|
}
|
|
516
|
-
logError(
|
|
696
|
+
logError('Authorization timed out. Please try again.');
|
|
517
697
|
process.exit(1);
|
|
518
698
|
}
|
|
519
699
|
catch (err) {
|
|
@@ -523,8 +703,8 @@ program
|
|
|
523
703
|
});
|
|
524
704
|
// Logout - remove API key from config
|
|
525
705
|
program
|
|
526
|
-
.command(
|
|
527
|
-
.description(
|
|
706
|
+
.command('logout')
|
|
707
|
+
.description('Log out by removing the saved API key')
|
|
528
708
|
.action(() => {
|
|
529
709
|
const globalOpts = getGlobalOptions();
|
|
530
710
|
setDebugEnabled(globalOpts.debug);
|
|
@@ -532,15 +712,15 @@ program
|
|
|
532
712
|
try {
|
|
533
713
|
const config = loadConfigFile();
|
|
534
714
|
if (!config.api_key) {
|
|
535
|
-
info(
|
|
715
|
+
info('Already logged out (no API key in config).');
|
|
536
716
|
return;
|
|
537
717
|
}
|
|
538
718
|
delete config.api_key;
|
|
539
719
|
saveConfigFile(config);
|
|
540
|
-
info(
|
|
720
|
+
info('Logged out. API key removed.');
|
|
541
721
|
output({
|
|
542
|
-
status:
|
|
543
|
-
message:
|
|
722
|
+
status: 'ok',
|
|
723
|
+
message: 'Logged out',
|
|
544
724
|
data: { config_path: getConfigPath() },
|
|
545
725
|
});
|
|
546
726
|
}
|
|
@@ -549,6 +729,160 @@ program
|
|
|
549
729
|
process.exit(1);
|
|
550
730
|
}
|
|
551
731
|
});
|
|
732
|
+
// Login info - show current user identity
|
|
733
|
+
program
|
|
734
|
+
.command('login-info')
|
|
735
|
+
.alias('me')
|
|
736
|
+
.description('Show current user info (email, plan)')
|
|
737
|
+
.action(async () => {
|
|
738
|
+
try {
|
|
739
|
+
const client = createClient();
|
|
740
|
+
const userInfo = await client.getMe();
|
|
741
|
+
output({
|
|
742
|
+
status: 'ok',
|
|
743
|
+
message: 'success',
|
|
744
|
+
data: userInfo,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
catch (err) {
|
|
748
|
+
logError(err.message);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
// Init OpenCode - generate opencode.json config
|
|
753
|
+
program
|
|
754
|
+
.command('init-opencode')
|
|
755
|
+
.description('Generate an opencode.json config file for OpenCode AI coding tool')
|
|
756
|
+
.option('--model <model>', 'Default model to use (e.g., claude-opus-4-6-1m)')
|
|
757
|
+
.option('-o, --out <path>', 'Output file path', pathModule.resolve('.opencode.json'))
|
|
758
|
+
.action(async (opts) => {
|
|
759
|
+
try {
|
|
760
|
+
const client = createClient();
|
|
761
|
+
const config = await client.getOpencodeConfig(opts.model);
|
|
762
|
+
const outputPath = pathModule.resolve(opts.out);
|
|
763
|
+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2) + '\n');
|
|
764
|
+
info(`Wrote OpenCode config to ${outputPath}`);
|
|
765
|
+
output({
|
|
766
|
+
status: 'ok',
|
|
767
|
+
message: `Config written to ${outputPath}`,
|
|
768
|
+
data: { path: outputPath },
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
logError(err.message);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
// ============================================
|
|
777
|
+
// Phone Call Command
|
|
778
|
+
// ============================================
|
|
779
|
+
program
|
|
780
|
+
.command('call')
|
|
781
|
+
.description('Make an AI phone call on your behalf. Validates prerequisites, resolves contact info, and initiates the call.')
|
|
782
|
+
.argument('<recipient>', 'Name of the person or business to call')
|
|
783
|
+
.requiredOption('-c, --contact_info <value>', 'Phone number (e.g. +1-555-123-4567) or Google Maps place_id')
|
|
784
|
+
.option('--is_place_id', 'Treat contact_info as a Google Maps place_id (for businesses)', false)
|
|
785
|
+
.requiredOption('-p, --purpose <value>', "Purpose of the call (e.g. 'Check reservation availability')")
|
|
786
|
+
.option('--dry-run', 'Only validate and resolve contact info, do not initiate the call')
|
|
787
|
+
.action(async (recipient, opts) => {
|
|
788
|
+
try {
|
|
789
|
+
const client = createClient();
|
|
790
|
+
if (opts.dryRun) {
|
|
791
|
+
// Dry run: just call the phone_call tool to validate and resolve
|
|
792
|
+
info('Dry run: validating prerequisites and resolving contact info...');
|
|
793
|
+
const result = await client.executeTool('phone_call', {
|
|
794
|
+
recipient,
|
|
795
|
+
contact_info: opts.contact_info,
|
|
796
|
+
is_place_id: opts.is_place_id,
|
|
797
|
+
purpose: opts.purpose,
|
|
798
|
+
});
|
|
799
|
+
if (result.status === 'ok' && result.session_state) {
|
|
800
|
+
const contacts = result.session_state.local_results;
|
|
801
|
+
if (contacts && contacts.length > 0) {
|
|
802
|
+
const c = contacts[0];
|
|
803
|
+
info('');
|
|
804
|
+
info('=== Contact Resolved ===');
|
|
805
|
+
info(` Name: ${c.title || recipient}`);
|
|
806
|
+
info(` Phone: ${c.phone || '(from place_id)'}`);
|
|
807
|
+
if (c.address)
|
|
808
|
+
info(` Address: ${c.address}`);
|
|
809
|
+
if (c.rating)
|
|
810
|
+
info(` Rating: ${c.rating}`);
|
|
811
|
+
info(` Purpose: ${opts.purpose}`);
|
|
812
|
+
info('');
|
|
813
|
+
info('Dry run complete. Use without --dry-run to initiate the call.');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
output(result);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// Full call: use the streaming initiate endpoint
|
|
820
|
+
info(`Initiating AI phone call to "${recipient}"...`);
|
|
821
|
+
info(` Purpose: ${opts.purpose}`);
|
|
822
|
+
info('');
|
|
823
|
+
const result = await client.requestStreaming('/phone_call/initiate', {
|
|
824
|
+
recipient,
|
|
825
|
+
contact_info: opts.contact_info,
|
|
826
|
+
is_place_id: opts.is_place_id,
|
|
827
|
+
purpose: opts.purpose,
|
|
828
|
+
}, (msg) => {
|
|
829
|
+
// Display streaming status updates to stderr
|
|
830
|
+
const step = msg.step || '';
|
|
831
|
+
if (step === 'resolved') {
|
|
832
|
+
const contact = msg.contact;
|
|
833
|
+
if (contact) {
|
|
834
|
+
info('Contact resolved:');
|
|
835
|
+
info(` Name: ${contact.title || recipient}`);
|
|
836
|
+
info(` Phone: ${contact.phone || '(from place_id)'}`);
|
|
837
|
+
if (contact.address)
|
|
838
|
+
info(` Address: ${contact.address}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else if (step === 'calling') {
|
|
842
|
+
info(`Dialing... (project: ${msg.project_id || 'unknown'})`);
|
|
843
|
+
}
|
|
844
|
+
else if (msg.heartbeat) {
|
|
845
|
+
const elapsed = msg.elapsed_seconds;
|
|
846
|
+
const callStep = msg.call_step;
|
|
847
|
+
info(` Call in progress... ${elapsed ? `(${elapsed}s)` : ''}${callStep && callStep !== 'in_progress' ? ` [${callStep}]` : ''}`);
|
|
848
|
+
}
|
|
849
|
+
else if (msg.message) {
|
|
850
|
+
info(` ${msg.message}`);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
// Display final result
|
|
854
|
+
const data = (result.data || {});
|
|
855
|
+
const callStatus = data.call_status || 'unknown';
|
|
856
|
+
const callSummary = data.call_summary || '';
|
|
857
|
+
const isFailure = result.status === 'error' ||
|
|
858
|
+
['failed', 'canceled', 'busy', 'no-answer'].includes(callStatus);
|
|
859
|
+
info('');
|
|
860
|
+
info(isFailure ? '=== Call Failed ===' : '=== Call Complete ===');
|
|
861
|
+
info(` Recipient: ${data.recipient || recipient}`);
|
|
862
|
+
info(` Status: ${callStatus}`);
|
|
863
|
+
if (data.duration_seconds)
|
|
864
|
+
info(` Duration: ${data.duration_seconds}s`);
|
|
865
|
+
info('');
|
|
866
|
+
info('=== Call Summary ===');
|
|
867
|
+
if (callSummary) {
|
|
868
|
+
info(callSummary);
|
|
869
|
+
}
|
|
870
|
+
else if (isFailure) {
|
|
871
|
+
info(result.message && result.message !== `Phone call completed (${callStatus})`
|
|
872
|
+
? result.message
|
|
873
|
+
: 'Call failed. The call could not be completed.');
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
info('Call completed. No summary available.');
|
|
877
|
+
}
|
|
878
|
+
info('');
|
|
879
|
+
output(result);
|
|
880
|
+
}
|
|
881
|
+
catch (err) {
|
|
882
|
+
logError(err.message);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
552
886
|
// ============================================
|
|
553
887
|
// Dynamic Tool Registration & Startup
|
|
554
888
|
// ============================================
|
|
@@ -568,17 +902,15 @@ async function main() {
|
|
|
568
902
|
const baseUrl = globalOpts.baseUrl;
|
|
569
903
|
const forceRefresh = globalOpts.refresh || false;
|
|
570
904
|
// Try to load tools from cache
|
|
571
|
-
let tools = forceRefresh
|
|
572
|
-
? null
|
|
573
|
-
: loadToolsCache(baseUrl);
|
|
905
|
+
let tools = forceRefresh ? null : loadToolsCache(baseUrl);
|
|
574
906
|
if (tools) {
|
|
575
907
|
debug(`Loaded ${tools.length} tools from cache`);
|
|
576
908
|
}
|
|
577
909
|
else {
|
|
578
910
|
// Need to fetch from server
|
|
579
911
|
debug(forceRefresh
|
|
580
|
-
?
|
|
581
|
-
:
|
|
912
|
+
? 'Force refreshing tools from server...'
|
|
913
|
+
: 'No cached tools found, fetching from server...');
|
|
582
914
|
// Only fetch if we have an API key
|
|
583
915
|
if (globalOpts.apiKey) {
|
|
584
916
|
try {
|
|
@@ -594,7 +926,7 @@ async function main() {
|
|
|
594
926
|
}
|
|
595
927
|
}
|
|
596
928
|
else {
|
|
597
|
-
debug(
|
|
929
|
+
debug('No API key available, skipping tool fetch');
|
|
598
930
|
}
|
|
599
931
|
}
|
|
600
932
|
// Register dynamic tool commands
|
|
@@ -606,7 +938,7 @@ async function main() {
|
|
|
606
938
|
// Parse and run
|
|
607
939
|
program.parse();
|
|
608
940
|
}
|
|
609
|
-
main().catch(
|
|
941
|
+
main().catch(err => {
|
|
610
942
|
logError(err.message);
|
|
611
943
|
process.exit(1);
|
|
612
944
|
});
|