@genspark/cli 1.5.0 → 1.5.2

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 (59) hide show
  1. package/README.md +22 -14
  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 +15 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +126 -1
  9. package/dist/client.js.map +1 -1
  10. package/dist/commands/aidrive-upload.d.ts +10 -7
  11. package/dist/commands/aidrive-upload.d.ts.map +1 -1
  12. package/dist/commands/aidrive-upload.js +35 -8
  13. package/dist/commands/aidrive-upload.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 +370 -34
  17. package/dist/index.js.map +1 -1
  18. package/dist/localFiles.d.ts +10 -0
  19. package/dist/localFiles.d.ts.map +1 -1
  20. package/dist/localFiles.js +17 -0
  21. package/dist/localFiles.js.map +1 -1
  22. package/dist/mesh/manifest.d.ts +4 -4
  23. package/dist/mesh/manifest.d.ts.map +1 -1
  24. package/dist/mesh/manifest.js +9 -9
  25. package/dist/mesh/manifest.js.map +1 -1
  26. package/dist/task-shim.d.ts +13 -0
  27. package/dist/task-shim.d.ts.map +1 -0
  28. package/dist/task-shim.js +40 -0
  29. package/dist/task-shim.js.map +1 -0
  30. package/dist/types.d.ts +1 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/docs/skills.md +9 -5
  33. package/package.json +1 -1
  34. package/skills/gsk-acp-agents/SKILL.md +2 -2
  35. package/skills/gsk-create-task/SKILL.md +6 -6
  36. package/skills/gsk-github/SKILL.md +4 -0
  37. package/skills/gsk-gmail/SKILL.md +4 -0
  38. package/skills/gsk-google-calendar/SKILL.md +5 -0
  39. package/skills/gsk-google-chat/SKILL.md +6 -2
  40. package/skills/gsk-google-docs/SKILL.md +4 -0
  41. package/skills/gsk-google-drive/SKILL.md +4 -0
  42. package/skills/gsk-google-sheets/SKILL.md +4 -0
  43. package/skills/gsk-google-slides/SKILL.md +51 -0
  44. package/skills/gsk-image-generation/SKILL.md +1 -1
  45. package/skills/gsk-microsoft-teams/SKILL.md +25 -9
  46. package/skills/gsk-notion/SKILL.md +4 -0
  47. package/skills/gsk-onedrive/SKILL.md +4 -0
  48. package/skills/gsk-outlook-calendar/SKILL.md +5 -0
  49. package/skills/gsk-outlook-email/SKILL.md +4 -0
  50. package/skills/gsk-shared/SKILL.md +5 -5
  51. package/skills/gsk-sharepoint/SKILL.md +4 -0
  52. package/skills/gsk-slack/SKILL.md +7 -1
  53. package/skills/gsk-task-artifacts/SKILL.md +35 -0
  54. package/skills/gsk-task-continue/SKILL.md +13 -10
  55. package/skills/gsk-task-info/SKILL.md +36 -0
  56. package/skills/gsk-task-status/SKILL.md +6 -6
  57. package/skills/gsk-task-stop/SKILL.md +35 -0
  58. package/skills/gsk-video-generation/SKILL.md +9 -6
  59. package/skills/gsk-youtube/SKILL.md +5 -4
package/dist/index.js CHANGED
@@ -26,14 +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 { classifyFileArg, awaitLocalFileReady, decideAfterWait, localFileNotReadyError, } from './localFiles.js';
31
- import { setDebugEnabled, setOutputFormat, debug, info, error as logError, output, } from './logger.js';
29
+ import { ApiClient, isAuthError, unauthenticatedRequest, } from './client.js';
30
+ import { classifyFileArg, awaitLocalFileReady, decideAfterWait, localFileNotReadyError, passthroughWarning, } from './localFiles.js';
31
+ import { setDebugEnabled, setOutputFormat, debug, info, warn, error as logError, output, } from './logger.js';
32
32
  import { loadConfigFile, getConfigPath, loadToolsCache, normalizeBaseUrl, saveToolsCache, setConfigPathOverride, updateConfigFile, } from './config.js';
33
33
  import { checkForUpdates } from './updater.js';
34
+ import { shimLegacyTaskInvocation } from './task-shim.js';
34
35
  import { getSandboxRuntime } from './runtime.js';
35
36
  import { registerDesignCommand } from './commands/design.js';
36
- import { pickExistingFileOperand } from './commands/aidrive-upload.js';
37
+ import { collectUploadFiles, pickExistingUploadOperand, } from './commands/aidrive-upload.js';
37
38
  import { parseLastSeenIndex, eventStdoutLine } from './commands/chat-events.js';
38
39
  import { mediaItemsKeyFor, mediaSummaryLines } from './mediaSummary.js';
39
40
  import { registerMeshCommand, getMeshInvocation, runMesh, } from './commands/mesh.js';
@@ -157,6 +158,7 @@ const CONTENT_TYPE_MAP = {
157
158
  '.png': 'image/png',
158
159
  '.gif': 'image/gif',
159
160
  '.webp': 'image/webp',
161
+ '.svg': 'image/svg+xml',
160
162
  '.pdf': 'application/pdf',
161
163
  '.txt': 'text/plain',
162
164
  '.json': 'application/json',
@@ -164,6 +166,20 @@ const CONTENT_TYPE_MAP = {
164
166
  '.css': 'text/css',
165
167
  '.js': 'application/javascript',
166
168
  '.ts': 'application/typescript',
169
+ '.jsx': 'text/javascript',
170
+ '.tsx': 'text/typescript',
171
+ '.vue': 'text/html',
172
+ '.py': 'text/x-python',
173
+ '.java': 'text/x-java-source',
174
+ '.go': 'text/x-go',
175
+ '.rs': 'text/x-rust',
176
+ '.c': 'text/x-c',
177
+ '.cc': 'text/x-c++',
178
+ '.cpp': 'text/x-c++',
179
+ '.h': 'text/x-c',
180
+ '.hpp': 'text/x-c++',
181
+ '.yaml': 'application/x-yaml',
182
+ '.yml': 'application/x-yaml',
167
183
  '.md': 'text/markdown',
168
184
  '.xml': 'application/xml',
169
185
  '.csv': 'text/csv',
@@ -173,6 +189,46 @@ const CONTENT_TYPE_MAP = {
173
189
  '.wav': 'audio/wav',
174
190
  '.webm': 'video/webm',
175
191
  };
192
+ /** Preserve interleaved --image/--file order; Commander groups options by key. */
193
+ export function collectDesignAttachmentArgs(argv) {
194
+ const result = [];
195
+ for (let index = 0; index < argv.length; index += 1) {
196
+ const token = argv[index];
197
+ const equalsMatch = token.match(/^--(image|file)=(.+)$/);
198
+ if (equalsMatch) {
199
+ result.push({
200
+ path: equalsMatch[2],
201
+ role: equalsMatch[1] === 'image' ? 'visual_reference' : 'context',
202
+ });
203
+ continue;
204
+ }
205
+ if (token !== '--image' && token !== '--file')
206
+ continue;
207
+ const role = token === '--image' ? 'visual_reference' : 'context';
208
+ while (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
209
+ result.push({ path: argv[index + 1], role });
210
+ index += 1;
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+ async function uploadDesignAttachments(values, client) {
216
+ const attachments = [];
217
+ // Upload the complete batch before executeTool: any failure aborts project
218
+ // creation/continuation rather than leaving a half-bound Design project.
219
+ for (const value of values) {
220
+ const localPath = expandHome(value.path);
221
+ if (!isLocalFilePath(localPath)) {
222
+ throw new Error(`File not found: ${value.path}`);
223
+ }
224
+ const name = pathModule.basename(localPath);
225
+ const ext = pathModule.extname(localPath).toLowerCase();
226
+ const mimeType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
227
+ const url = await uploadLocalFile(localPath, client);
228
+ attachments.push({ url, name, mime_type: mimeType, role: value.role });
229
+ }
230
+ return attachments;
231
+ }
176
232
  /**
177
233
  * Stream a local file to a URL using Node.js http/https.request +
178
234
  * fs.createReadStream so the file is never fully buffered in memory.
@@ -296,7 +352,7 @@ async function uploadLocalFile(filePath, client) {
296
352
  *
297
353
  * This avoids the roundabout blob-upload + download_file two-step approach.
298
354
  */
299
- async function uploadToAiDrive(filePath, uploadPath, override = false) {
355
+ async function uploadToAiDrive(filePath, uploadPath, override = false, workspace = 'personal') {
300
356
  const fileName = pathModule.basename(filePath);
301
357
  const ext = pathModule.extname(filePath).toLowerCase();
302
358
  const contentType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
@@ -304,7 +360,7 @@ async function uploadToAiDrive(filePath, uploadPath, override = false) {
304
360
  info(`Uploading ${fileName} (${formatFileSize(fileSize)}, ${contentType}) directly to AI Drive...`);
305
361
  const globalOpts = getGlobalOptions();
306
362
  const baseUrl = globalOpts.baseUrl.replace(/\/$/, '');
307
- const uploadUrl = `${baseUrl}/api/tool_cli/aidrive/upload?upload_path=${encodeURIComponent(uploadPath)}${override ? '&override=true' : ''}`;
363
+ const uploadUrl = `${baseUrl}/api/tool_cli/aidrive/upload?upload_path=${encodeURIComponent(uploadPath)}&workspace=${encodeURIComponent(workspace)}${override ? '&override=true' : ''}`;
308
364
  const headers = { 'Content-Type': contentType };
309
365
  if (globalOpts.apiKey)
310
366
  headers['X-Api-Key'] = globalOpts.apiKey;
@@ -362,7 +418,9 @@ async function resolveFileArg(value, client) {
362
418
  case 'passthrough':
363
419
  // No local evidence beyond the parent directory: let the backend try
364
420
  // it as an AI Drive path (its failure mode is a per-URL actionable
365
- // error, not a generic one).
421
+ // error, not a generic one). Warn first so a failed producer in the
422
+ // same batch is diagnosable from this call's own output.
423
+ warn(passthroughWarning(localPath, readiness));
366
424
  return value;
367
425
  }
368
426
  }
@@ -586,6 +644,12 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
586
644
  }
587
645
  const props = tool.parameters?.properties || {};
588
646
  const required = tool.parameters?.required || [];
647
+ // When --args-file is on the command line, required params may live in
648
+ // the file, which Commander cannot see — its parse-time required checks
649
+ // would reject the invocation before the file is ever read. Register
650
+ // everything optional in that case; the action handler re-validates the
651
+ // required set AFTER the file merge (#46011).
652
+ const argsFileInvoked = process.argv.some(a => a === '--args-file' || a.startsWith('--args-file='));
589
653
  // If primary_arg is set, make it a positional argument
590
654
  if (tool.cli.primary_arg && props[tool.cli.primary_arg]) {
591
655
  const param = props[tool.cli.primary_arg];
@@ -599,7 +663,9 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
599
663
  // usage say e.g. "missing required argument 'deck'" instead of 'value'
600
664
  // (#43556: the anonymous form sent agents guessing).
601
665
  const argName = tool.cli.primary_arg;
602
- const bracket = isRequired && !isServiceAction ? `<${argName}>` : `[${argName}]`;
666
+ const bracket = isRequired && !isServiceAction && !argsFileInvoked
667
+ ? `<${argName}>`
668
+ : `[${argName}]`;
603
669
  cmd.argument(bracket, param.description || argName);
604
670
  }
605
671
  // Add remaining params as --options
@@ -614,7 +680,8 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
614
680
  // requiredOption would block every invocation before the action handler
615
681
  // (and its collector) ever runs.
616
682
  const isRequired = tool.name === 'create_task' ||
617
- (tool.name === 'skills_save' && name === 'files')
683
+ (tool.name === 'skills_save' && name === 'files') ||
684
+ argsFileInvoked
618
685
  ? false
619
686
  : required.includes(name);
620
687
  const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
@@ -677,17 +744,34 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
677
744
  cmd.option('--attach <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
678
745
  cmd.option('--attachment <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
679
746
  }
747
+ // Quoting-hazard-free input path (#46011): a shell-invoked agent passing
748
+ // rich text in double quotes gets `$`-tokens expanded before gsk sees
749
+ // argv ("$150k" → "50k"). A JSON file / stdin never crosses shell quoting.
750
+ cmd.option('--args-file <path>', 'Read arguments from a JSON object file ("-" = stdin). File keys ' +
751
+ 'override flag values. Use for rich text (strings containing $, ' +
752
+ 'quotes, newlines) instead of double-quoted flags. Server ' +
753
+ 'parameters only — CLI-only options (e.g. --local_file) must ' +
754
+ 'still be passed as flags.');
680
755
  // Add -o/--output-file option for tools that produce downloadable files
681
756
  const outputFileKeys = tool.cli.output_file_keys || [];
682
757
  if (outputFileKeys.length > 0) {
683
758
  cmd.option('-o, --output-file <path>', 'Download the generated file to a local path');
684
759
  }
760
+ // task ask (task_continue) shares the async-first submit → --follow
761
+ // upgrades the stub into a live SSE follow, same as task create.
762
+ // (Named --follow, not --attach: tools with an attachments array param
763
+ // already own --attach as a file alias.)
764
+ if (tool.name === 'task_continue') {
765
+ cmd.option('--follow', 'After the async submit, follow the durable run live (SSE) and exit with the terminal result', false);
766
+ }
685
767
  // For create_task, add --acp flag to enter ACP stdio bridge mode
686
768
  if (tool.name === 'create_task') {
687
769
  cmd.option('--acp', 'Start as ACP (Agent Client Protocol) stdio agent instead of one-shot execution', false);
770
+ cmd.option('--follow', 'After the async submit, follow the durable run live (SSE) and exit with the terminal result', false);
688
771
  cmd.option('-o, --output-file <path>', 'Export the artifact (pptx/docx/xlsx) to a local file after task completes');
689
772
  cmd.option('--session-id <id>', 'ACP session ID (used with get-project)');
690
- cmd.option('--file <values...>', 'Import local CSV/Excel files into the task (auto-uploaded to blob storage)');
773
+ cmd.option('--file <values...>', 'Import local files (Design: structured context; other tasks: CSV/Excel file_urls)');
774
+ cmd.option('--image <values...>', 'Design only: upload ordered local visual references');
691
775
  cmd.addHelpText('after', '\nVirtual subcommands:\n' +
692
776
  ' get-project Get Genspark project info for an ACP session (local file lookup, no API call)\n' +
693
777
  ' --session-id <id> ACP session ID (required)\n' +
@@ -695,8 +779,15 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
695
779
  'Example:\n' +
696
780
  ' gsk task get-project --session-id <acp_session_id>\n');
697
781
  }
782
+ if (tool.name === 'task_continue') {
783
+ cmd.option('--image <values...>', 'Design only: append ordered local visual references');
784
+ cmd.option('--file <values...>', 'Design only: append ordered local context files');
785
+ }
698
786
  // For the aidrive tool, add client-side options used with --local_file upload
699
787
  if (tool.name === 'aidrive') {
788
+ if (!Object.hasOwn(props, 'workspace')) {
789
+ cmd.option('--workspace <value>', 'AI Drive workspace: personal or public_brain');
790
+ }
700
791
  cmd.option('--local_file <path>', 'Local file path to upload to AI Drive (supports files >5 MB; alternative to --file_content)');
701
792
  cmd.option('--override', 'Overwrite an existing file at the destination path (used with --local_file)', false);
702
793
  }
@@ -744,7 +835,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
744
835
  : undefined;
745
836
  if (tool.cli.primary_arg === 'action' &&
746
837
  Array.isArray(actionEnum) &&
747
- primaryArgValue === undefined) {
838
+ primaryArgValue === undefined &&
839
+ // The action may arrive via --args-file (read later in the
840
+ // handler) — don't short-circuit to the action listing then.
841
+ !opts.argsFile) {
748
842
  logError(tool.description);
749
843
  logError(`Available actions: ${actionEnum.join(', ')}`);
750
844
  logError(`Usage: gsk ${cmd.name()} <action> [options] ` +
@@ -790,8 +884,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
790
884
  await startAcpBridge(taskType, clientFactory().getOptions());
791
885
  return;
792
886
  }
793
- // For create_task (non-ACP): validate required params manually
794
- if (tool.name === 'create_task') {
887
+ // For create_task (non-ACP): validate required params manually.
888
+ // Skipped when --args-file is present — required params may live in
889
+ // the file, and the post-merge revalidation below enforces them.
890
+ if (tool.name === 'create_task' && !opts.argsFile) {
795
891
  for (const reqParam of required) {
796
892
  if (reqParam === tool.cli.primary_arg)
797
893
  continue;
@@ -826,7 +922,25 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
826
922
  const outputFilePath = opts.outputFile;
827
923
  delete opts.outputFile;
828
924
  delete opts.acp; // ACP flag is client-side only
925
+ // -o exports an artifact of the FINISHED project — an async submit
926
+ // stub has nothing to export yet, so an export request implies the
927
+ // live follow.
928
+ const attachTask = !!opts.follow || !!outputFilePath;
929
+ delete opts.follow; // client-side only
829
930
  delete opts.sessionId; // get-project flag is client-side only
931
+ const argsFilePath = opts.argsFile;
932
+ delete opts.argsFile; // client-side only; merged below
933
+ // Required checks were relaxed because --args-file appeared in argv;
934
+ // an explicitly EMPTY value (`--args-file=` / `--args-file ""`,
935
+ // parsed as '') would skip the merge AND the revalidation below — a
936
+ // validation bypass, not a degenerate no-op. Refuse it. undefined is
937
+ // deliberately allowed: argv detection can false-positive on a
938
+ // literal "--args-file" VALUE of another option, and the server-side
939
+ // required validation still covers that arm.
940
+ if (argsFilePath !== undefined && !argsFilePath) {
941
+ logError('--args-file requires a non-empty path (or "-" for stdin)');
942
+ process.exit(1);
943
+ }
830
944
  const args = { ...opts };
831
945
  // Set primary arg
832
946
  if (tool.cli.primary_arg && primaryArgValue !== undefined) {
@@ -857,6 +971,54 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
857
971
  delete args.attach;
858
972
  delete args.attachment;
859
973
  }
974
+ // Merge --args-file after every flag-derived write (positional,
975
+ // attachment aliases) so file keys genuinely win: the file is the
976
+ // deliberate rich payload, flags are the convenience layer (#46011).
977
+ // It sits before the numeric coercion below so file-provided
978
+ // numeric strings get the same schema-driven coercion flags do.
979
+ if (argsFilePath) {
980
+ let fileArgs;
981
+ try {
982
+ const { readArgsFile } = await import('./argsFile.js');
983
+ fileArgs = readArgsFile(argsFilePath);
984
+ }
985
+ catch (err) {
986
+ logError(err.message);
987
+ process.exit(1);
988
+ }
989
+ // The file carries SERVER parameters only. CLI-only options
990
+ // (--local_file, --file, --out, ...) are processed from flags by
991
+ // client-side blocks below that never see the file — honoring
992
+ // them here would silently do nothing, so warn and drop instead.
993
+ for (const key of Object.keys(fileArgs)) {
994
+ if (!(key in props)) {
995
+ warn(`--args-file: "${key}" is not a server parameter of ` +
996
+ `${tool.name} and was ignored — CLI-only options must be ` +
997
+ 'passed as flags.');
998
+ delete fileArgs[key];
999
+ }
1000
+ }
1001
+ Object.assign(args, fileArgs);
1002
+ // Required params were registered optional because the file may
1003
+ // carry them (Commander validates before the file is readable) —
1004
+ // re-validate the merged set now. CLI-populated params are
1005
+ // exempt: skills_save's `files` is schema-required but filled by
1006
+ // the directory collector further down, which deliberately owns
1007
+ // the value (file-wins does not apply to collector-owned params).
1008
+ for (const reqParam of required) {
1009
+ if (tool.name === 'skills_save' && reqParam === 'files')
1010
+ continue;
1011
+ if (args[reqParam] === undefined || args[reqParam] === null) {
1012
+ // The primary arg is a positional — there is no --<name>
1013
+ // flag for it, so the recovery hint must not invent one.
1014
+ const hint = reqParam === tool.cli.primary_arg
1015
+ ? `pass it as the positional argument or include it in --args-file`
1016
+ : `pass --${reqParam} or include it in --args-file`;
1017
+ logError(`Missing required parameter: ${reqParam} (${hint})`);
1018
+ process.exit(1);
1019
+ }
1020
+ }
1021
+ }
860
1022
  // Coerce numeric strings to numbers based on schema
861
1023
  for (const [name, param] of Object.entries(props)) {
862
1024
  if (args[name] !== undefined) {
@@ -941,6 +1103,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
941
1103
  info(`Warning: ${dir} carries provenance for ${collected.sidecarOwner} — ` +
942
1104
  `saving will fork this skill into your own catalog.`);
943
1105
  }
1106
+ // Collector-owned: `files` must be {path, content} read from the
1107
+ // local directory, so this assignment intentionally overrides any
1108
+ // --args-file value (the file-wins rule covers flag-shaped params
1109
+ // only, not CLI-populated ones).
944
1110
  args.files = collected.files;
945
1111
  }
946
1112
  // aidrive upload ergonomics: `gsk aidrive upload <path>` is the natural
@@ -954,10 +1120,21 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
954
1120
  // no usable source, fail with an actionable message that names
955
1121
  // --local_file rather than the opaque backend error.
956
1122
  if (tool.name === 'aidrive' &&
957
- primaryArgValue === 'upload' &&
1123
+ // args.action, not the positional: the action may arrive via
1124
+ // --args-file (sb-git's clone-url gate uses the same pattern).
1125
+ args.action === 'upload' &&
958
1126
  !opts.local_file &&
959
- !opts.file_content) {
960
- const pathOperand = pickExistingFileOperand(cmd.args.slice(1));
1127
+ // args, not opts: file_content is a server param and may have
1128
+ // arrived via --args-file (merged into args above).
1129
+ !args.file_content) {
1130
+ // Skip the first operand only when it really is the action: with
1131
+ // the action arriving via --args-file, Commander binds a bare
1132
+ // path to the [action] positional slot, so cmd.args[0] is itself
1133
+ // a path candidate.
1134
+ const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
1135
+ ? cmd.args.slice(1)
1136
+ : cmd.args;
1137
+ const pathOperand = pickExistingUploadOperand(operandCandidates);
961
1138
  if (pathOperand) {
962
1139
  opts.local_file = pathOperand;
963
1140
  }
@@ -973,10 +1150,11 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
973
1150
  // Stream the file directly to AI Drive via the dedicated tool_cli
974
1151
  // endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
975
1152
  if (tool.name === 'aidrive' &&
976
- primaryArgValue === 'upload' &&
1153
+ args.action === 'upload' &&
977
1154
  opts.local_file) {
978
1155
  const localFilePath = opts.local_file;
979
1156
  const override = !!opts.override;
1157
+ const workspace = args.workspace || 'personal';
980
1158
  if (!fs.existsSync(localFilePath)) {
981
1159
  logError(`File not found: ${localFilePath}`);
982
1160
  process.exit(1);
@@ -988,13 +1166,54 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
988
1166
  const uploadPath = rawUploadPath.startsWith('/')
989
1167
  ? rawUploadPath
990
1168
  : `/${rawUploadPath}`;
991
- // Stream directly to AI Drive — single request, no blob middleman
992
- const result = await uploadToAiDrive(localFilePath, uploadPath, override);
993
- output(result);
1169
+ const localStat = fs.statSync(localFilePath);
1170
+ const files = collectUploadFiles(localFilePath);
1171
+ if (localStat.isDirectory()) {
1172
+ if (files.length === 0) {
1173
+ logError(`Folder has no files to upload: ${localFilePath}`);
1174
+ process.exit(1);
1175
+ }
1176
+ info(`Uploading folder ${localFilePath} (${files.length} files)...`);
1177
+ const uploaded = [];
1178
+ for (const file of files) {
1179
+ const destination = pathModule.posix.join(uploadPath, file.relativePath);
1180
+ const result = await uploadToAiDrive(file.absolutePath, destination, override, workspace);
1181
+ if (result.status !== 'ok') {
1182
+ output(result);
1183
+ process.exit(1);
1184
+ }
1185
+ uploaded.push(destination);
1186
+ }
1187
+ output({
1188
+ status: 'ok',
1189
+ message: `Uploaded ${uploaded.length} files`,
1190
+ data: { workspace, root: uploadPath, files: uploaded },
1191
+ });
1192
+ }
1193
+ else {
1194
+ // Stream directly to AI Drive — single request, no blob middleman
1195
+ const result = await uploadToAiDrive(localFilePath, uploadPath, override, workspace);
1196
+ output(result);
1197
+ }
994
1198
  return;
995
1199
  }
996
- // create_task --file: upload local files and pass as file_urls
997
- if (tool.name === 'create_task' && opts.file) {
1200
+ const designAttachmentArgs = collectDesignAttachmentArgs(process.argv);
1201
+ const isDesignAttachmentCommand = designAttachmentArgs.length > 0 &&
1202
+ ((tool.name === 'create_task' && args.task_type === 'design') ||
1203
+ tool.name === 'task_continue');
1204
+ if (isDesignAttachmentCommand) {
1205
+ const uploaded = await uploadDesignAttachments(designAttachmentArgs, client);
1206
+ const existing = Array.isArray(args.attachments)
1207
+ ? args.attachments
1208
+ : [];
1209
+ args.attachments = [...existing, ...uploaded];
1210
+ delete args.image;
1211
+ delete args.file;
1212
+ }
1213
+ // Non-Design create_task --file keeps its CSV/Excel file_urls contract.
1214
+ if (tool.name === 'create_task' &&
1215
+ args.task_type !== 'design' &&
1216
+ opts.file) {
998
1217
  const fileValues = Array.isArray(opts.file)
999
1218
  ? opts.file
1000
1219
  : [opts.file];
@@ -1027,7 +1246,36 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
1027
1246
  }
1028
1247
  // Resolve local file paths to uploaded URLs
1029
1248
  const resolvedArgs = await resolveLocalFiles(args, client);
1030
- const result = await client.executeTool(tool.name, resolvedArgs);
1249
+ // The server owns the async-first default: create_task /
1250
+ // task_continue return a submit stub ({status: "submitted",
1251
+ // run_id, stream_url}) unless `--wait true` was passed. --attach
1252
+ // upgrades the stub into a live SSE follow that exits with the
1253
+ // run's terminal result — the work still executes server-side.
1254
+ let result = await client.executeTool(tool.name, resolvedArgs);
1255
+ const stubData = result.data;
1256
+ if (attachTask &&
1257
+ result.status === 'ok' &&
1258
+ stubData &&
1259
+ typeof stubData.stream_url === 'string' &&
1260
+ (stubData.status === 'submitted' ||
1261
+ stubData.status === 'duplicate_submit_converged')) {
1262
+ const meta = await client.attachGskTaskRun(stubData.stream_url);
1263
+ const runStatus = String(meta.status || '');
1264
+ const summary = meta.result_summary && typeof meta.result_summary === 'object'
1265
+ ? meta.result_summary
1266
+ : {};
1267
+ result = {
1268
+ status: runStatus === 'succeeded' ? 'ok' : 'error',
1269
+ message: runStatus === 'succeeded'
1270
+ ? 'success'
1271
+ : `task ended with status ${runStatus || 'unknown'}`,
1272
+ data: {
1273
+ ...summary,
1274
+ run_id: stubData.run_id,
1275
+ run_status: runStatus,
1276
+ },
1277
+ };
1278
+ }
1031
1279
  const isSbGitCloneUrl = tool.name === 'sb-git' &&
1032
1280
  resolvedArgs.action === 'clone-url';
1033
1281
  if (isSbGitCloneUrl &&
@@ -1179,7 +1427,10 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
1179
1427
  typeof result.data === 'object') {
1180
1428
  const data = result.data;
1181
1429
  const projectId = data.project_id;
1182
- const taskType = (primaryArgValue || args.type);
1430
+ // args.type is the value actually sent to the API: the positional
1431
+ // is copied into args and --args-file may override it (file wins),
1432
+ // so the export must key on the merged view, never the positional.
1433
+ const taskType = args.type;
1183
1434
  if (projectId && taskType) {
1184
1435
  info(`Exporting ${taskType} artifact...`);
1185
1436
  try {
@@ -1929,19 +2180,23 @@ _groupCommands.set('phone-call', phoneCallCmd);
1929
2180
  // gk_tool_cli_conversation) and attach as dynamic subcommands via the
1930
2181
  // seeded _groupCommands entry; this static parent only gives the group a
1931
2182
  // real description instead of the generated "Commands for chat".
1932
- const chatCmd = program
1933
- .command('chat')
1934
- .description('Headless agent conversations: start one and get ids back immediately, ' +
1935
- 'poll status for replies, send follow-ups, stop, delete, rename ' +
1936
- '(see subcommands).');
1937
- _groupCommands.set('chat', chatCmd);
1938
- // `chat events` is static (not server-registered): it needs real-time
2183
+ // The task command tree. Verbs are server-registered (create / status /
2184
+ // info / ask / answer / messages / wait / stop / delete / rename attach as
2185
+ // dynamic subcommands via the seeded group); this static parent gives the
2186
+ // group a description, and hosts the one static verb below.
2187
+ const taskCmd = program
2188
+ .command('task')
2189
+ .description('Agent tasks and conversations: create one (submits and returns in ' +
2190
+ 'seconds), poll status, read outputs, send follow-ups, answer ' +
2191
+ 'agent questions, stop, delete, rename (see subcommands).');
2192
+ _groupCommands.set('task', taskCmd);
2193
+ // `task events` is static (not server-registered): it needs real-time
1939
2194
  // line-by-line output the generic dynamic-command handler cannot provide
1940
2195
  // (that path buffers intermediate NDJSON messages and only prints the
1941
2196
  // final result).
1942
- chatCmd
2197
+ taskCmd
1943
2198
  .command('events <project_id>')
1944
- .description("Attach to the conversation's in-flight run and stream its events " +
2199
+ .description("Attach to the task's in-flight run and stream its events " +
1945
2200
  'live (one JSON line per event on stdout): the first line is a ' +
1946
2201
  'project_data snapshot event, buffered events since run start ' +
1947
2202
  'replay next, then live events follow until the run ends. Every ' +
@@ -1991,10 +2246,78 @@ chatCmd
1991
2246
  process.exit(1);
1992
2247
  }
1993
2248
  });
2249
+ // `task export` is static: it composes two calls (task_status to resolve
2250
+ // the task type, then /export_artifact) plus an optional local download —
2251
+ // the generic dynamic handler maps one verb to one server tool.
2252
+ taskCmd
2253
+ .command('export <project_id>')
2254
+ .description("Convert and download a task's deliverable (docs → docx/pdf, " +
2255
+ 'slides → pptx/pdf, sheets → xlsx). Prints the download URL; with ' +
2256
+ '-o <path> also saves the file locally. The task type is resolved ' +
2257
+ 'automatically from the project; `gsk task artifacts <project_id>` ' +
2258
+ 'lists what a task produced and which formats it can export.')
2259
+ .option('--format <format>', 'export format: auto | docx | pdf | pptx | xlsx (auto picks the ' +
2260
+ "task type's natural format)", 'auto')
2261
+ .option('-o, --output-file <path>', 'save the exported file to this path')
2262
+ .action(async (projectId, opts) => {
2263
+ try {
2264
+ const client = createClient();
2265
+ const statusResult = await client.executeTool('task_status', {
2266
+ id: projectId,
2267
+ });
2268
+ const statusData = statusResult.data;
2269
+ const taskType = statusResult.status === 'ok' && statusData
2270
+ ? statusData.task_type
2271
+ : undefined;
2272
+ if (!taskType) {
2273
+ output(statusResult.status === 'ok'
2274
+ ? {
2275
+ status: 'error',
2276
+ message: `Cannot resolve task type for ${projectId}`,
2277
+ data: null,
2278
+ }
2279
+ : statusResult);
2280
+ process.exit(1);
2281
+ }
2282
+ const exportResult = await client.exportArtifact(projectId, taskType, opts.format || 'auto');
2283
+ const expData = exportResult.data;
2284
+ if (exportResult.status === 'ok' && expData && opts.outputFile) {
2285
+ const downloadUrl = expData.download_url;
2286
+ if (downloadUrl) {
2287
+ expData.local_path = await downloadToFile(downloadUrl, opts.outputFile, client);
2288
+ }
2289
+ else {
2290
+ // The converter can report ok with an empty URL (e.g. the
2291
+ // share-link build failed server-side) — surface it instead
2292
+ // of printing ok with no file on disk.
2293
+ output({
2294
+ status: 'error',
2295
+ message: 'Export produced no download_url — nothing was saved ' +
2296
+ `to ${opts.outputFile}`,
2297
+ data: expData,
2298
+ });
2299
+ process.exit(1);
2300
+ }
2301
+ }
2302
+ output(exportResult);
2303
+ if (exportResult.status === 'error') {
2304
+ process.exit(1);
2305
+ }
2306
+ }
2307
+ catch (err) {
2308
+ logError(err.message);
2309
+ process.exit(1);
2310
+ }
2311
+ });
1994
2312
  // ============================================
1995
2313
  // Dynamic Tool Registration & Startup
1996
2314
  // ============================================
1997
2315
  async function main() {
2316
+ // Pre-tree `gsk task <type>` compat (see shimLegacyTaskInvocation).
2317
+ process.argv = [
2318
+ ...process.argv.slice(0, 2),
2319
+ ...shimLegacyTaskInvocation(process.argv.slice(2)),
2320
+ ];
1998
2321
  // `gsk mesh ...` is a native-binary passthrough — intercept it before the
1999
2322
  // tool-fetch path and npm self-update gating (it needs neither). Handling it
2000
2323
  // outside commander keeps the rest of the CLI's option parsing intact (see
@@ -2046,7 +2369,20 @@ async function main() {
2046
2369
  debug(`Fetched and cached ${tools.length} tools from server`);
2047
2370
  }
2048
2371
  catch (err) {
2049
- debug(`Failed to fetch tools: ${err.message}`);
2372
+ if (isAuthError(err)) {
2373
+ // A definitive auth verdict must be loud: swallowing it registers
2374
+ // zero dynamic commands and the user sees "unknown command" with
2375
+ // no hint that the token is the cause (#45982). Warn-and-continue
2376
+ // (not fail-fast) so built-in commands — `gsk login` above all —
2377
+ // still work with an expired token.
2378
+ warn(`API token rejected (HTTP ${err.httpStatus}) — ` +
2379
+ 'invalid or expired. Server-provided commands were NOT loaded; ' +
2380
+ 'if the command you need is missing, run `gsk login` to ' +
2381
+ 're-authenticate.');
2382
+ }
2383
+ else {
2384
+ debug(`Failed to fetch tools: ${err.message}`);
2385
+ }
2050
2386
  // Continue without dynamic commands - built-in commands still work
2051
2387
  }
2052
2388
  }