@genspark/cli 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ import type { ApiResponse } from '../types.js';
2
+ /**
3
+ * Name the operands of `gsk aidrive download <ai-drive-path> [local-path]`:
4
+ * first is the source, second the destination. With `explicitDrivePath` from
5
+ * `--path`/`-p`, the first operand is the destination instead.
6
+ */
7
+ export declare function resolveAidriveDownloadOperands(operands: readonly string[], explicitDrivePath?: string): {
8
+ drivePath?: string;
9
+ localPath?: string;
10
+ };
11
+ /**
12
+ * Accept either spelling of an AI-Drive location — `aidrive:///a/b.pdf` (how
13
+ * attachments arrive) or the plain `/a/b.pdf` — and return the plain path the
14
+ * API takes. A path without the scheme passes through untouched.
15
+ */
16
+ export declare function normalizeDrivePath(pathOrUrl: string): string;
17
+ /**
18
+ * Pull the signed download URL out of a `get_readable_url` response.
19
+ * `data.result` is prose ("Readable URL for <path>: <url>") and not meant to
20
+ * be parsed; the structured value lives in `session_state.aidrive_result`.
21
+ */
22
+ export declare function extractReadableUrl(response: Pick<ApiResponse, 'status' | 'session_state'>): string | undefined;
23
+ //# sourceMappingURL=aidrive-download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aidrive-download.d.ts","sourceRoot":"","sources":["../../src/commands/aidrive-download.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,iBAAiB,CAAC,EAAE,MAAM,GACzB;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ5C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,eAAe,CAAC,GACtD,MAAM,GAAG,SAAS,CAMpB"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Name the operands of `gsk aidrive download <ai-drive-path> [local-path]`:
3
+ * first is the source, second the destination. With `explicitDrivePath` from
4
+ * `--path`/`-p`, the first operand is the destination instead.
5
+ */
6
+ export function resolveAidriveDownloadOperands(operands, explicitDrivePath) {
7
+ const bareOperands = operands.filter((operand) => operand.length > 0 && !operand.startsWith('-'));
8
+ if (explicitDrivePath) {
9
+ return { drivePath: explicitDrivePath, localPath: bareOperands[0] };
10
+ }
11
+ return { drivePath: bareOperands[0], localPath: bareOperands[1] };
12
+ }
13
+ /**
14
+ * Accept either spelling of an AI-Drive location — `aidrive:///a/b.pdf` (how
15
+ * attachments arrive) or the plain `/a/b.pdf` — and return the plain path the
16
+ * API takes. A path without the scheme passes through untouched.
17
+ */
18
+ export function normalizeDrivePath(pathOrUrl) {
19
+ // The scheme normally carries an empty host, hence the slash run.
20
+ return pathOrUrl.replace(/^aidrive:\/+/, '/');
21
+ }
22
+ /**
23
+ * Pull the signed download URL out of a `get_readable_url` response.
24
+ * `data.result` is prose ("Readable URL for <path>: <url>") and not meant to
25
+ * be parsed; the structured value lives in `session_state.aidrive_result`.
26
+ */
27
+ export function extractReadableUrl(response) {
28
+ if (response.status !== 'ok')
29
+ return undefined;
30
+ const aidriveResult = response.session_state?.aidrive_result;
31
+ if (!aidriveResult || typeof aidriveResult !== 'object')
32
+ return undefined;
33
+ const url = aidriveResult.readable_url;
34
+ return typeof url === 'string' && url ? url : undefined;
35
+ }
36
+ //# sourceMappingURL=aidrive-download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aidrive-download.js","sourceRoot":"","sources":["../../src/commands/aidrive-download.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAA2B,EAC3B,iBAA0B;IAE1B,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAClC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAC5D,CAAA;IACD,IAAI,iBAAiB,EAAE,CAAC;QACtB,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;IACrE,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,kEAAkE;IAClE,OAAO,SAAS,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAA;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAuD;IAEvD,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IAC9C,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,EAAE,cAAc,CAAA;IAC5D,IAAI,CAAC,aAAa,IAAI,OAAO,aAAa,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IACzE,MAAM,GAAG,GAAI,aAAyC,CAAC,YAAY,CAAA;IACnE,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AACzD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG;AAqQH,KAAK,oBAAoB,GAAG,kBAAkB,GAAG,SAAS,GAAG,SAAS,CAAA;AAEtE,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,kFAAkF;AAClF,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EAAE,GACb,mBAAmB,EAAE,CAqBvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG;AAiRH,KAAK,oBAAoB,GAAG,kBAAkB,GAAG,SAAS,GAAG,SAAS,CAAA;AAEtE,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,kFAAkF;AAClF,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EAAE,GACb,mBAAmB,EAAE,CAqBvB"}
package/dist/index.js CHANGED
@@ -26,15 +26,19 @@ import * as os from 'os';
26
26
  import * as pathModule from 'path';
27
27
  import { createRequire } from 'module';
28
28
  import { spawn } from 'child_process';
29
+ import { Readable } from 'stream';
30
+ import { pipeline } from 'stream/promises';
29
31
  import { ApiClient, isAuthError, unauthenticatedRequest, } from './client.js';
30
32
  import { classifyFileArg, awaitLocalFileReady, decideAfterWait, localFileNotReadyError, passthroughWarning, urlsParamItems, } from './localFiles.js';
31
33
  import { setDebugEnabled, setOutputFormat, debug, info, warn, error as logError, output, } from './logger.js';
32
34
  import { loadConfigFile, getConfigPath, loadToolsCache, normalizeBaseUrl, saveToolsCache, setConfigPathOverride, updateConfigFile, } from './config.js';
33
35
  import { checkForUpdates } from './updater.js';
36
+ import { addRegisteredCommands, collectToolCommandTokens, staleCacheMissingToken, } from './staleCache.js';
34
37
  import { shimLegacyTaskInvocation } from './task-shim.js';
35
38
  import { getSandboxRuntime } from './runtime.js';
36
39
  import { registerDesignCommand } from './commands/design.js';
37
40
  import { collectUploadFiles, pickExistingUploadOperand, } from './commands/aidrive-upload.js';
41
+ import { resolveAidriveDownloadOperands, normalizeDrivePath, extractReadableUrl, } from './commands/aidrive-download.js';
38
42
  import { parseLastSeenIndex, eventStdoutLine } from './commands/chat-events.js';
39
43
  import { mediaItemsKeyFor, mediaSummaryLines } from './mediaSummary.js';
40
44
  import { registerMeshCommand, getMeshInvocation, runMesh, } from './commands/mesh.js';
@@ -320,6 +324,8 @@ async function streamFileUpload(filePath, uploadUrl, headers, fileSize, method =
320
324
  });
321
325
  }
322
326
  function formatFileSize(bytes) {
327
+ if (bytes >= 1024 ** 3)
328
+ return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
323
329
  if (bytes >= 1024 * 1024)
324
330
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
325
331
  return `${(bytes / 1024).toFixed(1)} KB`;
@@ -389,9 +395,13 @@ async function downloadToFile(url, savePath, client) {
389
395
  const resp = await fetch(downloadUrl);
390
396
  if (!resp.ok)
391
397
  throw new Error(`Download failed: HTTP ${resp.status}`);
392
- const buffer = Buffer.from(await resp.arrayBuffer());
393
- fs.writeFileSync(savePath, buffer);
394
- info(`Downloaded ${(buffer.length / 1024).toFixed(1)} KB ${savePath}`);
398
+ if (!resp.body)
399
+ throw new Error('Download failed: empty response body');
400
+ // Stream to disk: AI-Drive reads reach GB scale, past what the sandbox's
401
+ // swap-less RAM tolerates as a single buffer.
402
+ await pipeline(Readable.fromWeb(resp.body), fs.createWriteStream(savePath));
403
+ const sizeBytes = fs.statSync(savePath).size;
404
+ info(`Downloaded ${formatFileSize(sizeBytes)} → ${savePath}`);
395
405
  return pathModule.resolve(savePath);
396
406
  }
397
407
  function isLocalFilePath(value) {
@@ -817,12 +827,15 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
817
827
  cmd.option('--image <values...>', 'Design only: append ordered local visual references');
818
828
  cmd.option('--file <values...>', 'Design only: append ordered local context files');
819
829
  }
820
- // For the aidrive tool, add client-side options used with --local_file upload
830
+ // For the aidrive tool, add client-side options used by the --local_file
831
+ // upload and by `download`'s local destination.
821
832
  if (tool.name === 'aidrive') {
822
833
  if (!Object.hasOwn(props, 'workspace')) {
823
834
  cmd.option('--workspace <value>', 'AI Drive workspace: personal or public_brain');
824
835
  }
825
- cmd.option('--local_file <path>', 'Local file path to upload to AI Drive (supports files >5 MB; alternative to --file_content)');
836
+ cmd.option('--local_file <path>', 'upload: local file path to upload to AI Drive (supports files >5 MB; ' +
837
+ 'alternative to --file_content). download: local destination path ' +
838
+ '(same as the second operand; default ./<basename of the drive path>)');
826
839
  cmd.option('--override', 'Overwrite an existing file at the destination path (used with --local_file)', false);
827
840
  }
828
841
  // For sb-git: `--execute [dest]` turns `clone-url` from a snippet-print
@@ -1190,6 +1203,75 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
1190
1203
  process.exit(1);
1191
1204
  }
1192
1205
  }
1206
+ // Copy an AI-Drive file OUT to local disk. Client-side because only
1207
+ // this process can write the local filesystem: resolve the path to a
1208
+ // signed URL, then save it with the same streaming helper `-o` uses.
1209
+ // Never forwarded to the server, which answers a CLI too old to
1210
+ // intercept it with the two-call equivalent.
1211
+ if (tool.name === 'aidrive' && args.action === 'download') {
1212
+ const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
1213
+ ? invokedCmd.args.slice(1)
1214
+ : invokedCmd.args;
1215
+ const { drivePath: sourceOperand, localPath: operandDest } = resolveAidriveDownloadOperands(operandCandidates, typeof args.path === 'string' ? args.path : undefined);
1216
+ if (!sourceOperand) {
1217
+ logError('download needs the AI-Drive path: gsk aidrive download ' +
1218
+ '<ai-drive-path> [local-path] (or --path/-p). To save an ' +
1219
+ 'external url INTO AI Drive instead, use ' +
1220
+ '`download_file --file_url <url>`.');
1221
+ process.exit(1);
1222
+ }
1223
+ // Attachments arrive spelled `aidrive:///a/b.pdf`; take that as-is
1224
+ // rather than making the caller strip the scheme.
1225
+ const drivePath = normalizeDrivePath(sourceOperand);
1226
+ const urlResult = await client.executeTool('aidrive', {
1227
+ action: 'get_readable_url',
1228
+ path: drivePath,
1229
+ workspace: args.workspace,
1230
+ share: args.share,
1231
+ });
1232
+ const readableUrl = extractReadableUrl(urlResult);
1233
+ if (!readableUrl) {
1234
+ logError(`Failed to resolve AI-Drive path ${drivePath}: ${urlResult.message}`);
1235
+ process.exit(1);
1236
+ }
1237
+ const destPath = (typeof opts.local_file === 'string' ? opts.local_file : '') ||
1238
+ operandDest ||
1239
+ pathModule.basename(drivePath);
1240
+ const savedPath = await downloadToFile(readableUrl, destPath, client);
1241
+ output({
1242
+ status: 'ok',
1243
+ message: 'AI-Drive file downloaded to local disk',
1244
+ data: { ai_drive_path: drivePath, local_path: savedPath },
1245
+ });
1246
+ return;
1247
+ }
1248
+ // `download_file` saves an external URL INTO the drive and cannot copy
1249
+ // one out, yet it is the obvious spelling for "download a drive file"
1250
+ // and both attempts used to fail silently: its handler never consumes
1251
+ // `path`, and an `aidrive://` --file_url fails the server's URL
1252
+ // validation (16 consecutive no-op calls observed in #52631). Name
1253
+ // `download` instead of letting either shape exit 0 having done
1254
+ // nothing.
1255
+ if (tool.name === 'aidrive' && args.action === 'download_file') {
1256
+ const fileUrl = typeof args.file_url === 'string' ? args.file_url : '';
1257
+ if (fileUrl.startsWith('aidrive://')) {
1258
+ logError('download_file saves an external URL INTO AI Drive; it cannot ' +
1259
+ 'copy one out. Use: gsk aidrive download ' +
1260
+ `${fileUrl} <local-path>`);
1261
+ process.exit(1);
1262
+ }
1263
+ if (!fileUrl) {
1264
+ const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
1265
+ ? invokedCmd.args.slice(1)
1266
+ : invokedCmd.args;
1267
+ const { drivePath } = resolveAidriveDownloadOperands(operandCandidates, typeof args.path === 'string' ? args.path : undefined);
1268
+ logError('download_file needs `--file_url <external-url>` — it saves an ' +
1269
+ 'external URL INTO AI Drive. To copy a drive file OUT to ' +
1270
+ 'local disk: gsk aidrive download ' +
1271
+ `${drivePath || '<ai-drive-path>'} <local-path>`);
1272
+ process.exit(1);
1273
+ }
1274
+ }
1193
1275
  // aidrive: handle --local_file for the upload action.
1194
1276
  // Stream the file directly to AI Drive via the dedicated tool_cli
1195
1277
  // endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
@@ -2164,8 +2246,8 @@ async function phoneCallAction(recipient, opts, invokedAs) {
2164
2246
  info('=== Call Dispatched (async) ===');
2165
2247
  info(` Recipient: ${data.recipient || recipient}`);
2166
2248
  info(` Project ID: ${data.project_id}`);
2167
- info(` Poll: gsk call-status ${data.project_id}`);
2168
- info(` Stop: gsk call-hangup ${data.project_id}`);
2249
+ info(` Poll: gsk telephony call status ${data.project_id}`);
2250
+ info(` Stop: gsk telephony call hangup ${data.project_id}`);
2169
2251
  output(result);
2170
2252
  return;
2171
2253
  }
@@ -2436,6 +2518,41 @@ async function main() {
2436
2518
  const forceRefresh = globalOpts.refresh || false;
2437
2519
  // Try to load tools from cache
2438
2520
  let tools = forceRefresh ? null : loadToolsCache(baseUrl);
2521
+ // Needed both for the stale-cache check and for dynamic registration below.
2522
+ const vdProjectId = resolveVdProjectId();
2523
+ // Stale-cache self-heal (#51775): the server omits gatekeeper-gated tools
2524
+ // per user, so a cached tree written before a gate flipped ON rejects the
2525
+ // newly enabled command for up to the cache TTL. When argv clearly cannot
2526
+ // resolve against the cached tree, drop the cache and take the fetch
2527
+ // branch below — one manifest GET instead of a 24h blind window.
2528
+ // When the heal path drops the cache, keep the stale tree as a fallback:
2529
+ // a failed manifest GET must not leave the invocation with ZERO dynamic
2530
+ // commands (strictly worse than the stale cache it replaced).
2531
+ let staleFallbackTools = null;
2532
+ if (tools && globalOpts.apiKey) {
2533
+ let missing = null;
2534
+ try {
2535
+ const { operands } = program.parseOptions(process.argv.slice(2));
2536
+ const index = collectToolCommandTokens(tools, Boolean(vdProjectId));
2537
+ // phone-call / call-for-me dial by DEFAULT subcommand, so their second
2538
+ // token may be a recipient — never provably a miss. (`telephony call
2539
+ // <recipient>` is level-3 and outside the two-level check.)
2540
+ addRegisteredCommands(index, program.commands, [
2541
+ 'phone-call',
2542
+ 'call-for-me',
2543
+ ]);
2544
+ missing = staleCacheMissingToken(index, operands);
2545
+ }
2546
+ catch {
2547
+ // Conservative: any hiccup in the pre-parse means we keep the cache.
2548
+ }
2549
+ if (missing) {
2550
+ debug(`'${missing}' is not in the cached command tree — bypassing the ` +
2551
+ 'tools cache and refetching the manifest (stale-cache self-heal)');
2552
+ staleFallbackTools = tools;
2553
+ tools = null;
2554
+ }
2555
+ }
2439
2556
  if (tools) {
2440
2557
  debug(`Loaded ${tools.length} tools from cache`);
2441
2558
  }
@@ -2475,12 +2592,18 @@ async function main() {
2475
2592
  debug('No API key available, skipping tool fetch');
2476
2593
  }
2477
2594
  }
2595
+ // Self-heal refetch failed (network/auth): fall back to the stale cache
2596
+ // so this invocation still registers every previously known command.
2597
+ if (!tools && staleFallbackTools) {
2598
+ debug('Manifest refetch failed — falling back to the cached (possibly ' +
2599
+ 'stale) command tree');
2600
+ tools = staleFallbackTools;
2601
+ }
2478
2602
  // Register dynamic tool commands
2479
2603
  // Skip phone_call — the hardcoded `phone-call` command (dial default
2480
2604
  // subcommand) handles dialing with streaming UX the generic handler lacks.
2481
2605
  // Hide the `vd` subcommand group entirely unless the user has opted in
2482
2606
  // via config.vd_project_id or GSK_VD_PROJECT_ID env var.
2483
- const vdProjectId = resolveVdProjectId();
2484
2607
  if (tools) {
2485
2608
  for (const tool of tools) {
2486
2609
  if (tool.name === 'phone_call')