@genspark/cli 1.4.2 → 1.5.1

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.
Files changed (68) hide show
  1. package/README.md +23 -1
  2. package/dist/argsFile.d.ts +17 -0
  3. package/dist/argsFile.d.ts.map +1 -0
  4. package/dist/argsFile.js +38 -0
  5. package/dist/argsFile.js.map +1 -0
  6. package/dist/client.d.ts +10 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +19 -1
  9. package/dist/client.js.map +1 -1
  10. package/dist/config.d.ts +11 -8
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +100 -13
  13. package/dist/config.js.map +1 -1
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +290 -41
  17. package/dist/index.js.map +1 -1
  18. package/dist/localFiles.d.ts +62 -0
  19. package/dist/localFiles.d.ts.map +1 -0
  20. package/dist/localFiles.js +130 -0
  21. package/dist/localFiles.js.map +1 -0
  22. package/dist/logger.d.ts +8 -0
  23. package/dist/logger.d.ts.map +1 -1
  24. package/dist/logger.js +13 -0
  25. package/dist/logger.js.map +1 -1
  26. package/dist/mesh/checkupdate.js +3 -3
  27. package/dist/mesh/checkupdate.js.map +1 -1
  28. package/dist/runtime.d.ts +44 -0
  29. package/dist/runtime.d.ts.map +1 -0
  30. package/dist/runtime.js +62 -0
  31. package/dist/runtime.js.map +1 -0
  32. package/dist/types.d.ts +1 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/updater.d.ts +1 -0
  35. package/dist/updater.d.ts.map +1 -1
  36. package/dist/updater.js +10 -0
  37. package/dist/updater.js.map +1 -1
  38. package/docs/skills.md +24 -23
  39. package/package.json +1 -1
  40. package/skills/gsk-analyze-media/SKILL.md +2 -1
  41. package/skills/gsk-audio-generation/SKILL.md +1 -1
  42. package/skills/gsk-connector/SKILL.md +52 -0
  43. package/skills/gsk-create-task/SKILL.md +34 -1
  44. package/skills/gsk-github/SKILL.md +2 -0
  45. package/skills/gsk-gmail/SKILL.md +2 -0
  46. package/skills/gsk-google-calendar/SKILL.md +4 -1
  47. package/skills/gsk-google-chat/SKILL.md +4 -1
  48. package/skills/gsk-google-contacts/SKILL.md +2 -0
  49. package/skills/gsk-google-docs/SKILL.md +2 -0
  50. package/skills/gsk-google-drive/SKILL.md +3 -0
  51. package/skills/gsk-google-sheets/SKILL.md +4 -2
  52. package/skills/gsk-google-slides/SKILL.md +47 -0
  53. package/skills/gsk-hub/SKILL.md +15 -5
  54. package/skills/gsk-image-generation/SKILL.md +2 -2
  55. package/skills/gsk-microsoft-teams/SKILL.md +16 -11
  56. package/skills/gsk-notion/SKILL.md +2 -0
  57. package/skills/gsk-onedrive/SKILL.md +3 -0
  58. package/skills/gsk-outlook-calendar/SKILL.md +3 -1
  59. package/skills/gsk-outlook-contacts/SKILL.md +2 -0
  60. package/skills/gsk-outlook-email/SKILL.md +3 -10
  61. package/skills/gsk-salesforce/SKILL.md +2 -0
  62. package/skills/gsk-shared/SKILL.md +12 -1
  63. package/skills/gsk-sharepoint/SKILL.md +3 -0
  64. package/skills/gsk-sharepoint-upload/SKILL.md +2 -0
  65. package/skills/gsk-slack/SKILL.md +12 -7
  66. package/skills/gsk-task-continue/SKILL.md +4 -3
  67. package/skills/gsk-video-generation/SKILL.md +8 -7
  68. package/skills/gsk-youtube/SKILL.md +5 -4
package/dist/index.js CHANGED
@@ -26,13 +26,15 @@ 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 { ApiClient, unauthenticatedRequest } from './client.js';
30
- import { setDebugEnabled, setOutputFormat, debug, info, error as logError, output, } from './logger.js';
31
- import { loadConfigFile, saveConfigFile, getConfigPath, loadToolsCache, saveToolsCache, setConfigPathOverride, } from './config.js';
29
+ import { ApiClient, isAuthError, unauthenticatedRequest, } from './client.js';
30
+ import { classifyFileArg, awaitLocalFileReady, decideAfterWait, localFileNotReadyError, } from './localFiles.js';
31
+ import { setDebugEnabled, setOutputFormat, debug, info, warn, error as logError, output, } from './logger.js';
32
+ import { loadConfigFile, getConfigPath, loadToolsCache, normalizeBaseUrl, saveToolsCache, setConfigPathOverride, updateConfigFile, } from './config.js';
32
33
  import { checkForUpdates } from './updater.js';
34
+ import { getSandboxRuntime } from './runtime.js';
33
35
  import { registerDesignCommand } from './commands/design.js';
34
36
  import { pickExistingFileOperand } from './commands/aidrive-upload.js';
35
- import { parseLastSeenIndex, eventStdoutLine, } from './commands/chat-events.js';
37
+ import { parseLastSeenIndex, eventStdoutLine } from './commands/chat-events.js';
36
38
  import { mediaItemsKeyFor, mediaSummaryLines } from './mediaSummary.js';
37
39
  import { registerMeshCommand, getMeshInvocation, runMesh, } from './commands/mesh.js';
38
40
  import { executeCloneUrl, normalizeCloneUrlForGit, } from './commands/sb-git-clone.js';
@@ -104,9 +106,13 @@ function getGlobalOptions() {
104
106
  // API key priority: CLI > env > config file
105
107
  const apiKey = opts.apiKey || process.env.GSK_API_KEY || fileConfig.api_key;
106
108
  // Base URL priority: CLI > env > config file > default
107
- const baseUrl = opts.baseUrl !== DEFAULT_BASE_URL
109
+ const explicitBaseUrl = program.getOptionValueSource('baseUrl') === 'cli'
108
110
  ? opts.baseUrl
109
- : process.env.GSK_BASE_URL || fileConfig.base_url || DEFAULT_BASE_URL;
111
+ : undefined;
112
+ const baseUrl = normalizeBaseUrl(explicitBaseUrl ||
113
+ process.env.GSK_BASE_URL ||
114
+ fileConfig.base_url ||
115
+ DEFAULT_BASE_URL);
110
116
  // Timeout priority: CLI > config file > default
111
117
  const timeout = opts.timeout !== String(DEFAULT_TIMEOUT)
112
118
  ? parseInt(opts.timeout, 10)
@@ -151,6 +157,7 @@ const CONTENT_TYPE_MAP = {
151
157
  '.png': 'image/png',
152
158
  '.gif': 'image/gif',
153
159
  '.webp': 'image/webp',
160
+ '.svg': 'image/svg+xml',
154
161
  '.pdf': 'application/pdf',
155
162
  '.txt': 'text/plain',
156
163
  '.json': 'application/json',
@@ -158,6 +165,20 @@ const CONTENT_TYPE_MAP = {
158
165
  '.css': 'text/css',
159
166
  '.js': 'application/javascript',
160
167
  '.ts': 'application/typescript',
168
+ '.jsx': 'text/javascript',
169
+ '.tsx': 'text/typescript',
170
+ '.vue': 'text/html',
171
+ '.py': 'text/x-python',
172
+ '.java': 'text/x-java-source',
173
+ '.go': 'text/x-go',
174
+ '.rs': 'text/x-rust',
175
+ '.c': 'text/x-c',
176
+ '.cc': 'text/x-c++',
177
+ '.cpp': 'text/x-c++',
178
+ '.h': 'text/x-c',
179
+ '.hpp': 'text/x-c++',
180
+ '.yaml': 'application/x-yaml',
181
+ '.yml': 'application/x-yaml',
161
182
  '.md': 'text/markdown',
162
183
  '.xml': 'application/xml',
163
184
  '.csv': 'text/csv',
@@ -167,6 +188,46 @@ const CONTENT_TYPE_MAP = {
167
188
  '.wav': 'audio/wav',
168
189
  '.webm': 'video/webm',
169
190
  };
191
+ /** Preserve interleaved --image/--file order; Commander groups options by key. */
192
+ export function collectDesignAttachmentArgs(argv) {
193
+ const result = [];
194
+ for (let index = 0; index < argv.length; index += 1) {
195
+ const token = argv[index];
196
+ const equalsMatch = token.match(/^--(image|file)=(.+)$/);
197
+ if (equalsMatch) {
198
+ result.push({
199
+ path: equalsMatch[2],
200
+ role: equalsMatch[1] === 'image' ? 'visual_reference' : 'context',
201
+ });
202
+ continue;
203
+ }
204
+ if (token !== '--image' && token !== '--file')
205
+ continue;
206
+ const role = token === '--image' ? 'visual_reference' : 'context';
207
+ while (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
208
+ result.push({ path: argv[index + 1], role });
209
+ index += 1;
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+ async function uploadDesignAttachments(values, client) {
215
+ const attachments = [];
216
+ // Upload the complete batch before executeTool: any failure aborts project
217
+ // creation/continuation rather than leaving a half-bound Design project.
218
+ for (const value of values) {
219
+ const localPath = expandHome(value.path);
220
+ if (!isLocalFilePath(localPath)) {
221
+ throw new Error(`File not found: ${value.path}`);
222
+ }
223
+ const name = pathModule.basename(localPath);
224
+ const ext = pathModule.extname(localPath).toLowerCase();
225
+ const mimeType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
226
+ const url = await uploadLocalFile(localPath, client);
227
+ attachments.push({ url, name, mime_type: mimeType, role: value.role });
228
+ }
229
+ return attachments;
230
+ }
170
231
  /**
171
232
  * Stream a local file to a URL using Node.js http/https.request +
172
233
  * fs.createReadStream so the file is never fully buffered in memory.
@@ -338,9 +399,27 @@ function isLocalFilePath(value) {
338
399
  fs.existsSync(value));
339
400
  }
340
401
  async function resolveFileArg(value, client) {
341
- if (!isLocalFilePath(value))
402
+ // Classify the RAW value: a leading '~' is positive local evidence and must
403
+ // hit classifyFileArg's relative-marker branch before any expansion.
404
+ const kind = classifyFileArg(value);
405
+ if (kind === 'remote' || kind === 'aidrive')
342
406
  return value;
343
- return await uploadLocalFile(value, client);
407
+ // fs never expands '~' — expand only for the filesystem operations below.
408
+ const localPath = expandHome(value);
409
+ // Tolerate the same-batch create-then-read race (a parallel sibling tool
410
+ // call may still be writing the file) before deciding what the arg is.
411
+ const readiness = await awaitLocalFileReady(localPath);
412
+ switch (decideAfterWait(kind, readiness)) {
413
+ case 'upload':
414
+ return await uploadLocalFile(localPath, client);
415
+ case 'error':
416
+ throw new Error(localFileNotReadyError(localPath, readiness));
417
+ case 'passthrough':
418
+ // No local evidence beyond the parent directory: let the backend try
419
+ // it as an AI Drive path (its failure mode is a per-URL actionable
420
+ // error, not a generic one).
421
+ return value;
422
+ }
344
423
  }
345
424
  /**
346
425
  * Inline-base64 transport limit for email `attachments`. Anything larger
@@ -547,7 +626,9 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
547
626
  const prefix = groupName + '_';
548
627
  cmdName =
549
628
  tool.cli.subcommand ||
550
- (tool.name.startsWith(prefix) ? tool.name.slice(prefix.length) : tool.name);
629
+ (tool.name.startsWith(prefix)
630
+ ? tool.name.slice(prefix.length)
631
+ : tool.name);
551
632
  }
552
633
  const cmd = targetProgram
553
634
  .command(cmdName, cmdOpts?.hidden ? { hidden: true } : {})
@@ -560,6 +641,12 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
560
641
  }
561
642
  const props = tool.parameters?.properties || {};
562
643
  const required = tool.parameters?.required || [];
644
+ // When --args-file is on the command line, required params may live in
645
+ // the file, which Commander cannot see — its parse-time required checks
646
+ // would reject the invocation before the file is ever read. Register
647
+ // everything optional in that case; the action handler re-validates the
648
+ // required set AFTER the file merge (#46011).
649
+ const argsFileInvoked = process.argv.some(a => a === '--args-file' || a.startsWith('--args-file='));
563
650
  // If primary_arg is set, make it a positional argument
564
651
  if (tool.cli.primary_arg && props[tool.cli.primary_arg]) {
565
652
  const param = props[tool.cli.primary_arg];
@@ -573,7 +660,9 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
573
660
  // usage say e.g. "missing required argument 'deck'" instead of 'value'
574
661
  // (#43556: the anonymous form sent agents guessing).
575
662
  const argName = tool.cli.primary_arg;
576
- const bracket = isRequired && !isServiceAction ? `<${argName}>` : `[${argName}]`;
663
+ const bracket = isRequired && !isServiceAction && !argsFileInvoked
664
+ ? `<${argName}>`
665
+ : `[${argName}]`;
577
666
  cmd.argument(bracket, param.description || argName);
578
667
  }
579
668
  // Add remaining params as --options
@@ -588,7 +677,8 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
588
677
  // requiredOption would block every invocation before the action handler
589
678
  // (and its collector) ever runs.
590
679
  const isRequired = tool.name === 'create_task' ||
591
- (tool.name === 'skills_save' && name === 'files')
680
+ (tool.name === 'skills_save' && name === 'files') ||
681
+ argsFileInvoked
592
682
  ? false
593
683
  : required.includes(name);
594
684
  const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
@@ -651,6 +741,14 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
651
741
  cmd.option('--attach <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
652
742
  cmd.option('--attachment <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
653
743
  }
744
+ // Quoting-hazard-free input path (#46011): a shell-invoked agent passing
745
+ // rich text in double quotes gets `$`-tokens expanded before gsk sees
746
+ // argv ("$150k" → "50k"). A JSON file / stdin never crosses shell quoting.
747
+ cmd.option('--args-file <path>', 'Read arguments from a JSON object file ("-" = stdin). File keys ' +
748
+ 'override flag values. Use for rich text (strings containing $, ' +
749
+ 'quotes, newlines) instead of double-quoted flags. Server ' +
750
+ 'parameters only — CLI-only options (e.g. --local_file) must ' +
751
+ 'still be passed as flags.');
654
752
  // Add -o/--output-file option for tools that produce downloadable files
655
753
  const outputFileKeys = tool.cli.output_file_keys || [];
656
754
  if (outputFileKeys.length > 0) {
@@ -661,7 +759,8 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
661
759
  cmd.option('--acp', 'Start as ACP (Agent Client Protocol) stdio agent instead of one-shot execution', false);
662
760
  cmd.option('-o, --output-file <path>', 'Export the artifact (pptx/docx/xlsx) to a local file after task completes');
663
761
  cmd.option('--session-id <id>', 'ACP session ID (used with get-project)');
664
- cmd.option('--file <values...>', 'Import local CSV/Excel files into the task (auto-uploaded to blob storage)');
762
+ cmd.option('--file <values...>', 'Import local files (Design: structured context; other tasks: CSV/Excel file_urls)');
763
+ cmd.option('--image <values...>', 'Design only: upload ordered local visual references');
665
764
  cmd.addHelpText('after', '\nVirtual subcommands:\n' +
666
765
  ' get-project Get Genspark project info for an ACP session (local file lookup, no API call)\n' +
667
766
  ' --session-id <id> ACP session ID (required)\n' +
@@ -669,6 +768,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
669
768
  'Example:\n' +
670
769
  ' gsk task get-project --session-id <acp_session_id>\n');
671
770
  }
771
+ if (tool.name === 'task_continue') {
772
+ cmd.option('--image <values...>', 'Design only: append ordered local visual references');
773
+ cmd.option('--file <values...>', 'Design only: append ordered local context files');
774
+ }
672
775
  // For the aidrive tool, add client-side options used with --local_file upload
673
776
  if (tool.name === 'aidrive') {
674
777
  cmd.option('--local_file <path>', 'Local file path to upload to AI Drive (supports files >5 MB; alternative to --file_content)');
@@ -718,7 +821,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
718
821
  : undefined;
719
822
  if (tool.cli.primary_arg === 'action' &&
720
823
  Array.isArray(actionEnum) &&
721
- primaryArgValue === undefined) {
824
+ primaryArgValue === undefined &&
825
+ // The action may arrive via --args-file (read later in the
826
+ // handler) — don't short-circuit to the action listing then.
827
+ !opts.argsFile) {
722
828
  logError(tool.description);
723
829
  logError(`Available actions: ${actionEnum.join(', ')}`);
724
830
  logError(`Usage: gsk ${cmd.name()} <action> [options] ` +
@@ -764,8 +870,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
764
870
  await startAcpBridge(taskType, clientFactory().getOptions());
765
871
  return;
766
872
  }
767
- // For create_task (non-ACP): validate required params manually
768
- if (tool.name === 'create_task') {
873
+ // For create_task (non-ACP): validate required params manually.
874
+ // Skipped when --args-file is present — required params may live in
875
+ // the file, and the post-merge revalidation below enforces them.
876
+ if (tool.name === 'create_task' && !opts.argsFile) {
769
877
  for (const reqParam of required) {
770
878
  if (reqParam === tool.cli.primary_arg)
771
879
  continue;
@@ -801,6 +909,19 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
801
909
  delete opts.outputFile;
802
910
  delete opts.acp; // ACP flag is client-side only
803
911
  delete opts.sessionId; // get-project flag is client-side only
912
+ const argsFilePath = opts.argsFile;
913
+ delete opts.argsFile; // client-side only; merged below
914
+ // Required checks were relaxed because --args-file appeared in argv;
915
+ // an explicitly EMPTY value (`--args-file=` / `--args-file ""`,
916
+ // parsed as '') would skip the merge AND the revalidation below — a
917
+ // validation bypass, not a degenerate no-op. Refuse it. undefined is
918
+ // deliberately allowed: argv detection can false-positive on a
919
+ // literal "--args-file" VALUE of another option, and the server-side
920
+ // required validation still covers that arm.
921
+ if (argsFilePath !== undefined && !argsFilePath) {
922
+ logError('--args-file requires a non-empty path (or "-" for stdin)');
923
+ process.exit(1);
924
+ }
804
925
  const args = { ...opts };
805
926
  // Set primary arg
806
927
  if (tool.cli.primary_arg && primaryArgValue !== undefined) {
@@ -831,6 +952,54 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
831
952
  delete args.attach;
832
953
  delete args.attachment;
833
954
  }
955
+ // Merge --args-file after every flag-derived write (positional,
956
+ // attachment aliases) so file keys genuinely win: the file is the
957
+ // deliberate rich payload, flags are the convenience layer (#46011).
958
+ // It sits before the numeric coercion below so file-provided
959
+ // numeric strings get the same schema-driven coercion flags do.
960
+ if (argsFilePath) {
961
+ let fileArgs;
962
+ try {
963
+ const { readArgsFile } = await import('./argsFile.js');
964
+ fileArgs = readArgsFile(argsFilePath);
965
+ }
966
+ catch (err) {
967
+ logError(err.message);
968
+ process.exit(1);
969
+ }
970
+ // The file carries SERVER parameters only. CLI-only options
971
+ // (--local_file, --file, --out, ...) are processed from flags by
972
+ // client-side blocks below that never see the file — honoring
973
+ // them here would silently do nothing, so warn and drop instead.
974
+ for (const key of Object.keys(fileArgs)) {
975
+ if (!(key in props)) {
976
+ warn(`--args-file: "${key}" is not a server parameter of ` +
977
+ `${tool.name} and was ignored — CLI-only options must be ` +
978
+ 'passed as flags.');
979
+ delete fileArgs[key];
980
+ }
981
+ }
982
+ Object.assign(args, fileArgs);
983
+ // Required params were registered optional because the file may
984
+ // carry them (Commander validates before the file is readable) —
985
+ // re-validate the merged set now. CLI-populated params are
986
+ // exempt: skills_save's `files` is schema-required but filled by
987
+ // the directory collector further down, which deliberately owns
988
+ // the value (file-wins does not apply to collector-owned params).
989
+ for (const reqParam of required) {
990
+ if (tool.name === 'skills_save' && reqParam === 'files')
991
+ continue;
992
+ if (args[reqParam] === undefined || args[reqParam] === null) {
993
+ // The primary arg is a positional — there is no --<name>
994
+ // flag for it, so the recovery hint must not invent one.
995
+ const hint = reqParam === tool.cli.primary_arg
996
+ ? `pass it as the positional argument or include it in --args-file`
997
+ : `pass --${reqParam} or include it in --args-file`;
998
+ logError(`Missing required parameter: ${reqParam} (${hint})`);
999
+ process.exit(1);
1000
+ }
1001
+ }
1002
+ }
834
1003
  // Coerce numeric strings to numbers based on schema
835
1004
  for (const [name, param] of Object.entries(props)) {
836
1005
  if (args[name] !== undefined) {
@@ -915,6 +1084,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
915
1084
  info(`Warning: ${dir} carries provenance for ${collected.sidecarOwner} — ` +
916
1085
  `saving will fork this skill into your own catalog.`);
917
1086
  }
1087
+ // Collector-owned: `files` must be {path, content} read from the
1088
+ // local directory, so this assignment intentionally overrides any
1089
+ // --args-file value (the file-wins rule covers flag-shaped params
1090
+ // only, not CLI-populated ones).
918
1091
  args.files = collected.files;
919
1092
  }
920
1093
  // aidrive upload ergonomics: `gsk aidrive upload <path>` is the natural
@@ -928,10 +1101,21 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
928
1101
  // no usable source, fail with an actionable message that names
929
1102
  // --local_file rather than the opaque backend error.
930
1103
  if (tool.name === 'aidrive' &&
931
- primaryArgValue === 'upload' &&
1104
+ // args.action, not the positional: the action may arrive via
1105
+ // --args-file (sb-git's clone-url gate uses the same pattern).
1106
+ args.action === 'upload' &&
932
1107
  !opts.local_file &&
933
- !opts.file_content) {
934
- const pathOperand = pickExistingFileOperand(cmd.args.slice(1));
1108
+ // args, not opts: file_content is a server param and may have
1109
+ // arrived via --args-file (merged into args above).
1110
+ !args.file_content) {
1111
+ // Skip the first operand only when it really is the action: with
1112
+ // the action arriving via --args-file, Commander binds a bare
1113
+ // path to the [action] positional slot, so cmd.args[0] is itself
1114
+ // a path candidate.
1115
+ const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
1116
+ ? cmd.args.slice(1)
1117
+ : cmd.args;
1118
+ const pathOperand = pickExistingFileOperand(operandCandidates);
935
1119
  if (pathOperand) {
936
1120
  opts.local_file = pathOperand;
937
1121
  }
@@ -947,7 +1131,7 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
947
1131
  // Stream the file directly to AI Drive via the dedicated tool_cli
948
1132
  // endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
949
1133
  if (tool.name === 'aidrive' &&
950
- primaryArgValue === 'upload' &&
1134
+ args.action === 'upload' &&
951
1135
  opts.local_file) {
952
1136
  const localFilePath = opts.local_file;
953
1137
  const override = !!opts.override;
@@ -967,8 +1151,23 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
967
1151
  output(result);
968
1152
  return;
969
1153
  }
970
- // create_task --file: upload local files and pass as file_urls
971
- if (tool.name === 'create_task' && opts.file) {
1154
+ const designAttachmentArgs = collectDesignAttachmentArgs(process.argv);
1155
+ const isDesignAttachmentCommand = designAttachmentArgs.length > 0 &&
1156
+ ((tool.name === 'create_task' && args.task_type === 'design') ||
1157
+ tool.name === 'task_continue');
1158
+ if (isDesignAttachmentCommand) {
1159
+ const uploaded = await uploadDesignAttachments(designAttachmentArgs, client);
1160
+ const existing = Array.isArray(args.attachments)
1161
+ ? args.attachments
1162
+ : [];
1163
+ args.attachments = [...existing, ...uploaded];
1164
+ delete args.image;
1165
+ delete args.file;
1166
+ }
1167
+ // Non-Design create_task --file keeps its CSV/Excel file_urls contract.
1168
+ if (tool.name === 'create_task' &&
1169
+ args.task_type !== 'design' &&
1170
+ opts.file) {
972
1171
  const fileValues = Array.isArray(opts.file)
973
1172
  ? opts.file
974
1173
  : [opts.file];
@@ -1153,7 +1352,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
1153
1352
  typeof result.data === 'object') {
1154
1353
  const data = result.data;
1155
1354
  const projectId = data.project_id;
1156
- const taskType = (primaryArgValue || args.type);
1355
+ // args.type is the value actually sent to the API: the positional
1356
+ // is copied into args and --args-file may override it (file wins),
1357
+ // so the export must key on the merged view, never the positional.
1358
+ const taskType = args.type;
1157
1359
  if (projectId && taskType) {
1158
1360
  info(`Exporting ${taskType} artifact...`);
1159
1361
  try {
@@ -1186,8 +1388,7 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
1186
1388
  // web_search payloads are large and agent-consumed — indent-2 here
1187
1389
  // is pure token overhead on every SAS/Claw search
1188
1390
  output(result, {
1189
- compact: tool.name === 'web_search' ||
1190
- Boolean(mediaItemsKeyFor(tool.name)),
1391
+ compact: tool.name === 'web_search' || Boolean(mediaItemsKeyFor(tool.name)),
1191
1392
  });
1192
1393
  }
1193
1394
  catch (err) {
@@ -1364,10 +1565,10 @@ program
1364
1565
  const remainingSec = expires_in - elapsedSec;
1365
1566
  const tokenResp = await unauthenticatedRequest(baseUrl, `/api/cli_auth/token?code=${encodeURIComponent(device_code)}`, 'GET');
1366
1567
  if (tokenResp.status === 'approved' && tokenResp.api_key) {
1367
- // Save to config
1368
- const config = loadConfigFile();
1369
- config.api_key = tokenResp.api_key;
1370
- saveConfigFile(config);
1568
+ updateConfigFile({
1569
+ api_key: tokenResp.api_key,
1570
+ base_url: baseUrl,
1571
+ });
1371
1572
  info('Login successful! API key saved.');
1372
1573
  output({
1373
1574
  status: 'ok',
@@ -1405,8 +1606,7 @@ program
1405
1606
  info('Already logged out (no API key in config).');
1406
1607
  return;
1407
1608
  }
1408
- delete config.api_key;
1409
- saveConfigFile(config);
1609
+ updateConfigFile({}, ['api_key']);
1410
1610
  info('Logged out. API key removed.');
1411
1611
  output({
1412
1612
  status: 'ok',
@@ -1439,6 +1639,25 @@ program
1439
1639
  process.exit(1);
1440
1640
  }
1441
1641
  });
1642
+ // Runtime - report the sandbox environment this CLI process runs in
1643
+ program
1644
+ .command('runtime')
1645
+ .description('Show the Genspark sandbox runtime this CLI is running in ' +
1646
+ '(reads the /opt/genspark template marker; all-null outside a sandbox)')
1647
+ .action(() => {
1648
+ const runtime = getSandboxRuntime();
1649
+ output({
1650
+ status: 'ok',
1651
+ message: 'success',
1652
+ data: {
1653
+ family: runtime.family,
1654
+ version: runtime.version,
1655
+ is_sb_box: runtime.isSbBox,
1656
+ is_super_agent_sandbox: runtime.isSuperAgentSandbox,
1657
+ is_genspark_sandbox: runtime.isGensparkSandbox,
1658
+ },
1659
+ });
1660
+ });
1442
1661
  // Init OpenCode - generate opencode.json config
1443
1662
  program
1444
1663
  .command('init-opencode')
@@ -1652,6 +1871,13 @@ function _generateContextMd(projectRoot, relSkillsDir, skillDirs) {
1652
1871
 
1653
1872
  Read [gsk-shared](${sharedSkill}) for authentication, global flags, and output conventions.
1654
1873
 
1874
+ ## Connectors
1875
+
1876
+ For any third-party integration (Gmail, Slack, Notion, HubSpot, custom MCP
1877
+ servers, ...), start with [gsk-connector](${relSkillsDir}/gsk-connector/SKILL.md) —
1878
+ one fixed \`list\` → \`search\` → \`tools <id>\` → \`call <id>\` flow covers every
1879
+ connector instead of memorizing per-service commands below.
1880
+
1655
1881
  ## Available Skills
1656
1882
 
1657
1883
  ${skillList}
@@ -1892,12 +2118,16 @@ _groupCommands.set('chat', chatCmd);
1892
2118
  chatCmd
1893
2119
  .command('events <project_id>')
1894
2120
  .description("Attach to the conversation's in-flight run and stream its events " +
1895
- 'live (one JSON line per event on stdout): buffered events since ' +
1896
- 'run start replay first, then live events follow until the run ' +
1897
- 'ends. Prints no_running_agent when the conversation is idle. ' +
1898
- 'After a disconnect, reattach with --last-seen <highest ' +
1899
- '_event_index already processed> to skip the replayed prefix.')
1900
- .option('--last-seen <index>', 'resume cursor: only replay events with _event_index greater than this')
2121
+ 'live (one JSON line per event on stdout): the first line is a ' +
2122
+ 'project_data snapshot event, buffered events since run start ' +
2123
+ 'replay next, then live events follow until the run ends. Every ' +
2124
+ 'event line carries an _event_index. Prints no_running_agent when ' +
2125
+ 'the conversation is idle. Upstream segment ends during long runs ' +
2126
+ 'are reattached automatically; after a real disconnect, reattach ' +
2127
+ 'with --last-seen <highest _event_index already processed> to ' +
2128
+ 'skip the replayed prefix.')
2129
+ .option('--last-seen <index>', 'resume cursor: only replay events with _event_index greater than ' +
2130
+ 'this (take it from the last event line of the previous stream)')
1901
2131
  .action(async (projectId, opts) => {
1902
2132
  try {
1903
2133
  const lastSeen = parseLastSeenIndex(opts.lastSeen);
@@ -1921,6 +2151,11 @@ chatCmd
1921
2151
  else if (msg.heartbeat !== undefined) {
1922
2152
  debug(` Heartbeat #${msg.heartbeat}`);
1923
2153
  }
2154
+ else if (msg.debug && typeof msg.message === 'string') {
2155
+ // Server-side notices (e.g. transparent segment reattach)
2156
+ // are context, not payload — stderr, --debug only.
2157
+ debug(` ${msg.message}`);
2158
+ }
1924
2159
  });
1925
2160
  output(result);
1926
2161
  if (result.status === 'error') {
@@ -1944,11 +2179,12 @@ async function main() {
1944
2179
  if (meshArgs !== null) {
1945
2180
  program.parseOptions(process.argv);
1946
2181
  setDebugEnabled(getGlobalOptions().debug);
1947
- // Forward an explicit `--base-url` flag only (commander leaves it at
1948
- // DEFAULT_BASE_URL when unset); env/config/prod-default resolution happens
1949
- // inside the mesh layer so it doesn't inherit gsk's localhost default.
2182
+ // Forward an explicit `--base-url` flag only; env/config/default resolution
2183
+ // happens inside the mesh layer.
1950
2184
  const rawBase = program.opts().baseUrl;
1951
- const baseUrlOverride = rawBase && rawBase !== DEFAULT_BASE_URL ? rawBase : undefined;
2185
+ const baseUrlOverride = rawBase && program.getOptionValueSource('baseUrl') === 'cli'
2186
+ ? normalizeBaseUrl(rawBase)
2187
+ : undefined;
1952
2188
  await runMesh(meshArgs, baseUrlOverride);
1953
2189
  return;
1954
2190
  }
@@ -1986,7 +2222,20 @@ async function main() {
1986
2222
  debug(`Fetched and cached ${tools.length} tools from server`);
1987
2223
  }
1988
2224
  catch (err) {
1989
- debug(`Failed to fetch tools: ${err.message}`);
2225
+ if (isAuthError(err)) {
2226
+ // A definitive auth verdict must be loud: swallowing it registers
2227
+ // zero dynamic commands and the user sees "unknown command" with
2228
+ // no hint that the token is the cause (#45982). Warn-and-continue
2229
+ // (not fail-fast) so built-in commands — `gsk login` above all —
2230
+ // still work with an expired token.
2231
+ warn(`API token rejected (HTTP ${err.httpStatus}) — ` +
2232
+ 'invalid or expired. Server-provided commands were NOT loaded; ' +
2233
+ 'if the command you need is missing, run `gsk login` to ' +
2234
+ 're-authenticate.');
2235
+ }
2236
+ else {
2237
+ debug(`Failed to fetch tools: ${err.message}`);
2238
+ }
1990
2239
  // Continue without dynamic commands - built-in commands still work
1991
2240
  }
1992
2241
  }