@genspark/cli 1.5.3 → 1.6.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.
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +720 -641
- package/dist/index.js.map +1 -1
- package/dist/serviceActions.d.ts +44 -0
- package/dist/serviceActions.d.ts.map +1 -0
- package/dist/serviceActions.js +95 -0
- package/dist/serviceActions.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/skills.md +3 -2
- package/package.json +1 -1
- package/skills/gsk-aidrive/SKILL.md +6 -0
- package/skills/gsk-create-task/SKILL.md +5 -5
- package/skills/gsk-genteam/SKILL.md +71 -0
- package/skills/gsk-google-chat/SKILL.md +1 -1
- package/skills/gsk-hub/SKILL.md +2 -1
- package/skills/gsk-image-generation/SKILL.md +1 -1
- package/skills/gsk-microsoft-teams/SKILL.md +1 -1
- package/skills/gsk-slack/SKILL.md +1 -1
- package/skills/gsk-task-continue/SKILL.md +2 -2
- package/skills/gsk-task-status/SKILL.md +2 -2
- package/skills/gsk-video-generation/SKILL.md +5 -5
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { parseLastSeenIndex, eventStdoutLine } from './commands/chat-events.js';
|
|
|
39
39
|
import { mediaItemsKeyFor, mediaSummaryLines } from './mediaSummary.js';
|
|
40
40
|
import { registerMeshCommand, getMeshInvocation, runMesh, } from './commands/mesh.js';
|
|
41
41
|
import { executeCloneUrl, normalizeCloneUrlForGit, } from './commands/sb-git-clone.js';
|
|
42
|
+
import { actionScopedDescription, serviceActionViews, } from './serviceActions.js';
|
|
42
43
|
const require = createRequire(import.meta.url);
|
|
43
44
|
const { version: VERSION } = require('../package.json');
|
|
44
45
|
const DEFAULT_BASE_URL = 'https://www.genspark.ai';
|
|
@@ -576,6 +577,50 @@ function parseBooleanFlagValue(value) {
|
|
|
576
577
|
return false;
|
|
577
578
|
throw new InvalidArgumentError(`expected true/false. If '${value}' was meant as an argument, place it before the flag or pass an explicit value (--flag true ${value}).`);
|
|
578
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Register one schema parameter as a Commander option.
|
|
582
|
+
*
|
|
583
|
+
* Shared by the union-flag command and the per-action subcommands so both
|
|
584
|
+
* views parse a given flag identically. Booleans accept an optional literal
|
|
585
|
+
* value (`--flag` alone is true, `--flag false` is false — a bare presence
|
|
586
|
+
* flag reads the literal as a stray positional and stays true, the silent
|
|
587
|
+
* inversion that once sent a phone number into a place-id lookup). Single
|
|
588
|
+
* -char aliases are short flags (-n); multi-char aliases must be long flags
|
|
589
|
+
* (--page) — commander treats `-page` as a literal short flag. A flag with
|
|
590
|
+
* an injected default always uses `option` (never `requiredOption`):
|
|
591
|
+
* Commander treats a defaulted flag as satisfied.
|
|
592
|
+
*/
|
|
593
|
+
function addSchemaOption(cmd, name, param, o) {
|
|
594
|
+
const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
|
|
595
|
+
const shortPrefix = o.shortAlias
|
|
596
|
+
? `${o.shortAlias.length > 1 ? '--' : '-'}${o.shortAlias}, `
|
|
597
|
+
: '';
|
|
598
|
+
const desc = o.description || param.description || name;
|
|
599
|
+
if (paramType === 'array') {
|
|
600
|
+
const flag = `${shortPrefix}--${name} <values...>`;
|
|
601
|
+
if (o.isRequired) {
|
|
602
|
+
cmd.requiredOption(flag, desc);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
cmd.option(flag, desc);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (paramType === 'boolean') {
|
|
609
|
+
cmd.option(`${shortPrefix}--${name} [value]`, desc, parseBooleanFlagValue);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const flag = `${shortPrefix}--${name} <value>`;
|
|
613
|
+
if (o.injectedDefault !== undefined) {
|
|
614
|
+
cmd.option(flag, desc, o.injectedDefault);
|
|
615
|
+
}
|
|
616
|
+
else if (o.isRequired) {
|
|
617
|
+
cmd.requiredOption(flag, desc);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
cmd.option(flag, desc);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
579
624
|
/**
|
|
580
625
|
* Register a dynamic tool command from a server-provided schema.
|
|
581
626
|
*
|
|
@@ -656,6 +701,17 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
656
701
|
// everything optional in that case; the action handler re-validates the
|
|
657
702
|
// required set AFTER the file merge (#46011).
|
|
658
703
|
const argsFileInvoked = process.argv.some(a => a === '--args-file' || a.startsWith('--args-file='));
|
|
704
|
+
// Per-action subcommand mode (issue #51377): when the server sends the
|
|
705
|
+
// per-action parameter map, each action registers below as a real
|
|
706
|
+
// Commander subcommand with flags scoped to it. The parent must then NOT
|
|
707
|
+
// own the union flags: in Commander's default (non-positional) mode a
|
|
708
|
+
// parent consumes every option it knows from the whole line BEFORE
|
|
709
|
+
// dispatching, so union flags on the parent would swallow `--chat_id`
|
|
710
|
+
// away from `gsk teams read_chat --chat_id ...`. With no union flags the
|
|
711
|
+
// tokens flow down as unknowns to the dispatched subcommand, which
|
|
712
|
+
// accepts its own and rejects wrong-action flags at parse time. Hidden
|
|
713
|
+
// legacy forms keep the union view.
|
|
714
|
+
const actionViews = cmdOpts?.deprecated ? null : serviceActionViews(tool);
|
|
659
715
|
// If primary_arg is set, make it a positional argument
|
|
660
716
|
if (tool.cli.primary_arg && props[tool.cli.primary_arg]) {
|
|
661
717
|
const param = props[tool.cli.primary_arg];
|
|
@@ -674,9 +730,14 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
674
730
|
: `[${argName}]`;
|
|
675
731
|
cmd.argument(bracket, param.description || argName);
|
|
676
732
|
}
|
|
677
|
-
// Add remaining params as --options
|
|
733
|
+
// Add remaining params as --options (union view). Skipped in per-action
|
|
734
|
+
// subcommand mode — see the actionViews comment above; the parent then
|
|
735
|
+
// keeps only the [action] positional (bare invocation → action listing;
|
|
736
|
+
// unknown action names still reach the server) and --args-file.
|
|
678
737
|
const aliases = tool.cli.parameter_aliases || {};
|
|
679
738
|
for (const [name, param] of Object.entries(props)) {
|
|
739
|
+
if (actionViews)
|
|
740
|
+
break;
|
|
680
741
|
if (name === tool.cli.primary_arg)
|
|
681
742
|
continue;
|
|
682
743
|
// For create_task, make params optional so --acp mode works without them.
|
|
@@ -690,51 +751,16 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
690
751
|
argsFileInvoked
|
|
691
752
|
? false
|
|
692
753
|
: required.includes(name);
|
|
693
|
-
const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
|
|
694
|
-
// Single-char aliases are short flags (-n); multi-char aliases must be
|
|
695
|
-
// long flags (--page) — commander treats `-page` as a literal short flag,
|
|
696
|
-
// so the documented `--page` form would fail with "unknown option".
|
|
697
|
-
const shortAlias = aliases[name];
|
|
698
|
-
const shortPrefix = shortAlias
|
|
699
|
-
? `${shortAlias.length > 1 ? '--' : '-'}${shortAlias}, `
|
|
700
|
-
: '';
|
|
701
754
|
// Auto-inject default for vd_project_id on tools in the `vd` group so
|
|
702
|
-
// users don't retype it on every call.
|
|
703
|
-
// always use `option` (not `requiredOption`) — Commander treats a flag
|
|
704
|
-
// with a default value as satisfied and skips the required-missing
|
|
705
|
-
// check, which is the behaviour we want here.
|
|
755
|
+
// users don't retype it on every call.
|
|
706
756
|
const injectedDefault = tool.cli.group === 'vd' && name === 'vd_project_id'
|
|
707
757
|
? vdProjectIdDefault
|
|
708
758
|
: undefined;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
else {
|
|
715
|
-
cmd.option(flag, param.description || name);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
else if (paramType === 'boolean') {
|
|
719
|
-
// Accept an optional literal value: `--flag` alone means true, but
|
|
720
|
-
// `--flag false` must mean false. With a bare presence flag Commander
|
|
721
|
-
// treats the literal as a stray positional and the flag still reads
|
|
722
|
-
// true — that silent inversion once sent a phone number into a Maps
|
|
723
|
-
// place-id lookup (`--is_place_id false`).
|
|
724
|
-
cmd.option(`${shortPrefix}--${name} [value]`, param.description || name, parseBooleanFlagValue);
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
const flag = `${shortPrefix}--${name} <value>`;
|
|
728
|
-
if (injectedDefault !== undefined) {
|
|
729
|
-
cmd.option(flag, param.description || name, injectedDefault);
|
|
730
|
-
}
|
|
731
|
-
else if (isRequired) {
|
|
732
|
-
cmd.requiredOption(flag, param.description || name);
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
cmd.option(flag, param.description || name);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
759
|
+
addSchemaOption(cmd, name, param, {
|
|
760
|
+
isRequired,
|
|
761
|
+
shortAlias: aliases[name],
|
|
762
|
+
injectedDefault,
|
|
763
|
+
});
|
|
738
764
|
}
|
|
739
765
|
// For tools that expose an `attachments` array (email send/draft/reply/
|
|
740
766
|
// forward across gmail + outlook), also surface human-friendly aliases
|
|
@@ -746,7 +772,7 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
746
772
|
(Array.isArray(props.attachments.type)
|
|
747
773
|
? props.attachments.type.includes('array')
|
|
748
774
|
: props.attachments.type === 'array');
|
|
749
|
-
if (hasAttachmentsParam) {
|
|
775
|
+
if (hasAttachmentsParam && !actionViews) {
|
|
750
776
|
cmd.option('--attach <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
|
|
751
777
|
cmd.option('--attachment <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
|
|
752
778
|
}
|
|
@@ -758,9 +784,11 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
758
784
|
'quotes, newlines) instead of double-quoted flags. Server ' +
|
|
759
785
|
'parameters only — CLI-only options (e.g. --local_file) must ' +
|
|
760
786
|
'still be passed as flags.');
|
|
761
|
-
// Add -o/--output-file option for tools that produce downloadable files
|
|
787
|
+
// Add -o/--output-file option for tools that produce downloadable files.
|
|
788
|
+
// In per-action subcommand mode the subcommands own the flag — on the
|
|
789
|
+
// parent it would be consumed pre-dispatch and lost (see actionViews).
|
|
762
790
|
const outputFileKeys = tool.cli.output_file_keys || [];
|
|
763
|
-
if (outputFileKeys.length > 0) {
|
|
791
|
+
if (outputFileKeys.length > 0 && !actionViews) {
|
|
764
792
|
cmd.option('-o, --output-file <path>', 'Download the generated file to a local path');
|
|
765
793
|
}
|
|
766
794
|
// task ask (task_continue) shares the async-first submit → --follow
|
|
@@ -826,657 +854,708 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
826
854
|
if (tool.name === 'skills_save') {
|
|
827
855
|
cmd.argument('[path]', 'Local skill directory to save (default: current directory)');
|
|
828
856
|
}
|
|
829
|
-
// Generic action handler
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
? props[tool.cli.primary_arg]?.enum
|
|
841
|
-
: undefined;
|
|
842
|
-
if (tool.cli.primary_arg === 'action' &&
|
|
843
|
-
Array.isArray(actionEnum) &&
|
|
844
|
-
primaryArgValue === undefined &&
|
|
845
|
-
// The action may arrive via --args-file (read later in the
|
|
846
|
-
// handler) — don't short-circuit to the action listing then.
|
|
847
|
-
!opts.argsFile) {
|
|
848
|
-
logError(tool.description);
|
|
849
|
-
logError(`Available actions: ${actionEnum.join(', ')}`);
|
|
850
|
-
logError(`Usage: gsk ${cmd.name()} <action> [options] ` +
|
|
851
|
-
`(run \`gsk ${cmd.name()} --help\` for per-action options)`);
|
|
852
|
-
process.exit(1);
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
// get-project: local file lookup for ACP session project info.
|
|
856
|
-
// Handled here (not as a Commander subcommand) to avoid
|
|
857
|
-
// Commander's subcommand-vs-positional-arg conflict.
|
|
858
|
-
if (tool.name === 'create_task' && primaryArgValue === 'get-project') {
|
|
859
|
-
const { loadPersistedSession, sanitizeId } = await import('./acp-serve.js');
|
|
860
|
-
const rawId = opts.sessionId || '';
|
|
861
|
-
const sessionId = sanitizeId(rawId);
|
|
862
|
-
if (!sessionId) {
|
|
863
|
-
logError('Invalid or missing --session-id');
|
|
864
|
-
process.exit(1);
|
|
865
|
-
}
|
|
866
|
-
const data = loadPersistedSession(sessionId);
|
|
867
|
-
if (!data) {
|
|
868
|
-
output({ status: 'pending' });
|
|
869
|
-
process.exitCode = 1;
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
const baseUrl = getGlobalOptions().baseUrl.replace(/\/$/, '');
|
|
873
|
-
output({
|
|
874
|
-
session_id: data.sessionId,
|
|
875
|
-
project_id: data.projectId,
|
|
876
|
-
project_url: `${baseUrl}/agents?id=${data.projectId}`,
|
|
877
|
-
task_type: data.taskType,
|
|
878
|
-
});
|
|
879
|
-
return;
|
|
857
|
+
// Generic invocation body — shared by the union command's action handler
|
|
858
|
+
// and (for service tools) the per-action subcommands, which call it with
|
|
859
|
+
// the action name pre-bound. `invokedCmd` is whichever Commander command
|
|
860
|
+
// actually parsed this invocation (operand access below reads its .args).
|
|
861
|
+
const runToolInvocation = async (primaryArgValue, opts, invokedCmd = cmd) => {
|
|
862
|
+
{
|
|
863
|
+
// Commander passes opts as second arg when there's an argument,
|
|
864
|
+
// or as first arg when there's no argument
|
|
865
|
+
if (typeof primaryArgValue === 'object' && primaryArgValue !== null) {
|
|
866
|
+
opts = primaryArgValue;
|
|
867
|
+
primaryArgValue = undefined;
|
|
880
868
|
}
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
869
|
+
// Service tool invoked with no action → list the available actions
|
|
870
|
+
// instead of failing on the (intentionally optional) action positional.
|
|
871
|
+
const actionEnum = tool.cli.primary_arg
|
|
872
|
+
? props[tool.cli.primary_arg]?.enum
|
|
873
|
+
: undefined;
|
|
874
|
+
if (tool.cli.primary_arg === 'action' &&
|
|
875
|
+
Array.isArray(actionEnum) &&
|
|
876
|
+
primaryArgValue === undefined &&
|
|
877
|
+
// The action may arrive via --args-file (read later in the
|
|
878
|
+
// handler) — don't short-circuit to the action listing then.
|
|
879
|
+
!opts.argsFile) {
|
|
880
|
+
logError(tool.description);
|
|
881
|
+
logError(`Available actions: ${actionEnum.join(', ')}`);
|
|
882
|
+
logError(`Usage: gsk ${cmd.name()} <action> [options] ` +
|
|
883
|
+
`(run \`gsk ${cmd.name()} --help\` for per-action options)`);
|
|
884
|
+
process.exit(1);
|
|
892
885
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
886
|
+
try {
|
|
887
|
+
// get-project: local file lookup for ACP session project info.
|
|
888
|
+
// Handled here (not as a Commander subcommand) to avoid
|
|
889
|
+
// Commander's subcommand-vs-positional-arg conflict.
|
|
890
|
+
if (tool.name === 'create_task' && primaryArgValue === 'get-project') {
|
|
891
|
+
const { loadPersistedSession, sanitizeId } = await import('./acp-serve.js');
|
|
892
|
+
const rawId = opts.sessionId || '';
|
|
893
|
+
const sessionId = sanitizeId(rawId);
|
|
894
|
+
if (!sessionId) {
|
|
895
|
+
logError('Invalid or missing --session-id');
|
|
902
896
|
process.exit(1);
|
|
903
897
|
}
|
|
898
|
+
const data = loadPersistedSession(sessionId);
|
|
899
|
+
if (!data) {
|
|
900
|
+
output({ status: 'pending' });
|
|
901
|
+
process.exitCode = 1;
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const baseUrl = getGlobalOptions().baseUrl.replace(/\/$/, '');
|
|
905
|
+
output({
|
|
906
|
+
session_id: data.sessionId,
|
|
907
|
+
project_id: data.projectId,
|
|
908
|
+
project_url: `${baseUrl}/agents?id=${data.projectId}`,
|
|
909
|
+
task_type: data.taskType,
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
904
912
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
// Extract and remove client-side-only options before sending to API
|
|
928
|
-
const outputFilePath = opts.outputFile;
|
|
929
|
-
delete opts.outputFile;
|
|
930
|
-
delete opts.acp; // ACP flag is client-side only
|
|
931
|
-
// -o exports an artifact of the FINISHED project — an async submit
|
|
932
|
-
// stub has nothing to export yet, so an export request implies the
|
|
933
|
-
// live follow.
|
|
934
|
-
const attachTask = !!opts.follow || !!outputFilePath;
|
|
935
|
-
delete opts.follow; // client-side only
|
|
936
|
-
delete opts.sessionId; // get-project flag is client-side only
|
|
937
|
-
const argsFilePath = opts.argsFile;
|
|
938
|
-
delete opts.argsFile; // client-side only; merged below
|
|
939
|
-
// Required checks were relaxed because --args-file appeared in argv;
|
|
940
|
-
// an explicitly EMPTY value (`--args-file=` / `--args-file ""`,
|
|
941
|
-
// parsed as '') would skip the merge AND the revalidation below — a
|
|
942
|
-
// validation bypass, not a degenerate no-op. Refuse it. undefined is
|
|
943
|
-
// deliberately allowed: argv detection can false-positive on a
|
|
944
|
-
// literal "--args-file" VALUE of another option, and the server-side
|
|
945
|
-
// required validation still covers that arm.
|
|
946
|
-
if (argsFilePath !== undefined && !argsFilePath) {
|
|
947
|
-
logError('--args-file requires a non-empty path (or "-" for stdin)');
|
|
948
|
-
process.exit(1);
|
|
949
|
-
}
|
|
950
|
-
const args = { ...opts };
|
|
951
|
-
// Set primary arg
|
|
952
|
-
if (tool.cli.primary_arg && primaryArgValue !== undefined) {
|
|
953
|
-
args[tool.cli.primary_arg] = primaryArgValue;
|
|
954
|
-
}
|
|
955
|
-
// Merge --attach / --attachment aliases into args.attachments
|
|
956
|
-
// before sending to the API so the server sees a single field.
|
|
957
|
-
// Order: --attachments first (canonical), then --attach, then
|
|
958
|
-
// --attachment, mirroring how Commander stores variadics.
|
|
959
|
-
if (hasAttachmentsParam) {
|
|
960
|
-
const merged = [];
|
|
961
|
-
for (const key of ['attachments', 'attach', 'attachment']) {
|
|
962
|
-
const val = opts[key];
|
|
963
|
-
if (Array.isArray(val)) {
|
|
964
|
-
for (const item of val) {
|
|
965
|
-
if (typeof item === 'string')
|
|
966
|
-
merged.push(item);
|
|
913
|
+
// ACP mode: enter stdio bridge instead of one-shot execution
|
|
914
|
+
if (tool.name === 'create_task' && opts.acp) {
|
|
915
|
+
const taskType = primaryArgValue;
|
|
916
|
+
if (!taskType) {
|
|
917
|
+
logError('Task type is required for ACP mode. Usage: gsk task <type> --acp');
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
delete opts.acp;
|
|
921
|
+
const { startAcpBridge } = await import('./acp-serve.js');
|
|
922
|
+
await startAcpBridge(taskType, clientFactory().getOptions());
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
// For create_task (non-ACP): validate required params manually.
|
|
926
|
+
// Skipped when --args-file is present — required params may live in
|
|
927
|
+
// the file, and the post-merge revalidation below enforces them.
|
|
928
|
+
if (tool.name === 'create_task' && !opts.argsFile) {
|
|
929
|
+
for (const reqParam of required) {
|
|
930
|
+
if (reqParam === tool.cli.primary_arg)
|
|
931
|
+
continue;
|
|
932
|
+
if (opts[reqParam] === undefined || opts[reqParam] === null) {
|
|
933
|
+
logError(`Missing required option: --${reqParam}`);
|
|
934
|
+
process.exit(1);
|
|
967
935
|
}
|
|
968
936
|
}
|
|
969
937
|
}
|
|
970
|
-
|
|
971
|
-
|
|
938
|
+
const client = clientFactory();
|
|
939
|
+
// Deprecated legacy form: warn on stderr (agents only relearn from
|
|
940
|
+
// invocation-time feedback, not from hidden help text) and tag the
|
|
941
|
+
// request so the server's deprecation telemetry can count it.
|
|
942
|
+
if (cmdOpts?.deprecated) {
|
|
943
|
+
// Group-qualify the legacy label so `gsk phone-call status` and
|
|
944
|
+
// the flat `gsk call_status` count as distinct legacy forms.
|
|
945
|
+
const legacyName = tool.cli.group
|
|
946
|
+
? `${tool.cli.group} ${cmdName}`
|
|
947
|
+
: cmdName;
|
|
948
|
+
console.error(`[DEPRECATED] \`gsk ${legacyName}\` still works but is deprecated — ` +
|
|
949
|
+
`use \`${cmdOpts.deprecated.replacement}\` instead.`);
|
|
950
|
+
client.setInvokedAs(`${legacyName} (legacy)`);
|
|
972
951
|
}
|
|
973
952
|
else {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
// Merge --args-file after every flag-derived write (positional,
|
|
981
|
-
// attachment aliases) so file keys genuinely win: the file is the
|
|
982
|
-
// deliberate rich payload, flags are the convenience layer (#46011).
|
|
983
|
-
// It sits before the numeric coercion below so file-provided
|
|
984
|
-
// numeric strings get the same schema-driven coercion flags do.
|
|
985
|
-
if (argsFilePath) {
|
|
986
|
-
let fileArgs;
|
|
987
|
-
try {
|
|
988
|
-
const { readArgsFile } = await import('./argsFile.js');
|
|
989
|
-
fileArgs = readArgsFile(argsFilePath);
|
|
953
|
+
client.setInvokedAs(tool.cli.path && tool.cli.path.length >= 2
|
|
954
|
+
? tool.cli.path.join(' ')
|
|
955
|
+
: tool.cli.group
|
|
956
|
+
? `${tool.cli.group} ${cmdName}`
|
|
957
|
+
: cmdName);
|
|
990
958
|
}
|
|
991
|
-
|
|
992
|
-
|
|
959
|
+
// Extract and remove client-side-only options before sending to API
|
|
960
|
+
const outputFilePath = opts.outputFile;
|
|
961
|
+
delete opts.outputFile;
|
|
962
|
+
delete opts.acp; // ACP flag is client-side only
|
|
963
|
+
// -o exports an artifact of the FINISHED project — an async submit
|
|
964
|
+
// stub has nothing to export yet, so an export request implies the
|
|
965
|
+
// live follow.
|
|
966
|
+
const attachTask = !!opts.follow || !!outputFilePath;
|
|
967
|
+
delete opts.follow; // client-side only
|
|
968
|
+
delete opts.sessionId; // get-project flag is client-side only
|
|
969
|
+
// In per-action subcommand mode the PARENT registers --args-file
|
|
970
|
+
// (for the action-in-file invocation) and therefore consumes the
|
|
971
|
+
// flag pre-dispatch even when a subcommand runs — fall back to the
|
|
972
|
+
// parent's parsed value so `gsk teams read_chat --args-file f.json`
|
|
973
|
+
// is honored, never silently dropped.
|
|
974
|
+
const argsFilePath = (opts.argsFile ??
|
|
975
|
+
(invokedCmd !== cmd ? cmd.opts().argsFile : undefined));
|
|
976
|
+
delete opts.argsFile; // client-side only; merged below
|
|
977
|
+
// Required checks were relaxed because --args-file appeared in argv;
|
|
978
|
+
// an explicitly EMPTY value (`--args-file=` / `--args-file ""`,
|
|
979
|
+
// parsed as '') would skip the merge AND the revalidation below — a
|
|
980
|
+
// validation bypass, not a degenerate no-op. Refuse it. undefined is
|
|
981
|
+
// deliberately allowed: argv detection can false-positive on a
|
|
982
|
+
// literal "--args-file" VALUE of another option, and the server-side
|
|
983
|
+
// required validation still covers that arm.
|
|
984
|
+
if (argsFilePath !== undefined && !argsFilePath) {
|
|
985
|
+
logError('--args-file requires a non-empty path (or "-" for stdin)');
|
|
993
986
|
process.exit(1);
|
|
994
987
|
}
|
|
995
|
-
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
988
|
+
const args = { ...opts };
|
|
989
|
+
// Set primary arg
|
|
990
|
+
if (tool.cli.primary_arg && primaryArgValue !== undefined) {
|
|
991
|
+
args[tool.cli.primary_arg] = primaryArgValue;
|
|
992
|
+
}
|
|
993
|
+
// Merge --attach / --attachment aliases into args.attachments
|
|
994
|
+
// before sending to the API so the server sees a single field.
|
|
995
|
+
// Order: --attachments first (canonical), then --attach, then
|
|
996
|
+
// --attachment, mirroring how Commander stores variadics.
|
|
997
|
+
if (hasAttachmentsParam) {
|
|
998
|
+
const merged = [];
|
|
999
|
+
for (const key of ['attachments', 'attach', 'attachment']) {
|
|
1000
|
+
const val = opts[key];
|
|
1001
|
+
if (Array.isArray(val)) {
|
|
1002
|
+
for (const item of val) {
|
|
1003
|
+
if (typeof item === 'string')
|
|
1004
|
+
merged.push(item);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (merged.length > 0) {
|
|
1009
|
+
args.attachments = merged;
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
delete args.attachments;
|
|
1005
1013
|
}
|
|
1014
|
+
// The aliases are CLI-only; the server doesn't know about them.
|
|
1015
|
+
delete args.attach;
|
|
1016
|
+
delete args.attachment;
|
|
1006
1017
|
}
|
|
1007
|
-
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const hint = reqParam === tool.cli.primary_arg
|
|
1021
|
-
? `pass it as the positional argument or include it in --args-file`
|
|
1022
|
-
: `pass --${reqParam} or include it in --args-file`;
|
|
1023
|
-
logError(`Missing required parameter: ${reqParam} (${hint})`);
|
|
1018
|
+
// Merge --args-file after every flag-derived write (positional,
|
|
1019
|
+
// attachment aliases) so file keys genuinely win: the file is the
|
|
1020
|
+
// deliberate rich payload, flags are the convenience layer (#46011).
|
|
1021
|
+
// It sits before the numeric coercion below so file-provided
|
|
1022
|
+
// numeric strings get the same schema-driven coercion flags do.
|
|
1023
|
+
if (argsFilePath) {
|
|
1024
|
+
let fileArgs;
|
|
1025
|
+
try {
|
|
1026
|
+
const { readArgsFile } = await import('./argsFile.js');
|
|
1027
|
+
fileArgs = readArgsFile(argsFilePath);
|
|
1028
|
+
}
|
|
1029
|
+
catch (err) {
|
|
1030
|
+
logError(err.message);
|
|
1024
1031
|
process.exit(1);
|
|
1025
1032
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1033
|
+
// The file carries SERVER parameters only. CLI-only options
|
|
1034
|
+
// (--local_file, --file, --out, ...) are processed from flags by
|
|
1035
|
+
// client-side blocks below that never see the file — honoring
|
|
1036
|
+
// them here would silently do nothing, so warn and drop instead.
|
|
1037
|
+
for (const key of Object.keys(fileArgs)) {
|
|
1038
|
+
if (!(key in props)) {
|
|
1039
|
+
warn(`--args-file: "${key}" is not a server parameter of ` +
|
|
1040
|
+
`${tool.name} and was ignored — CLI-only options must be ` +
|
|
1041
|
+
'passed as flags.');
|
|
1042
|
+
delete fileArgs[key];
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
Object.assign(args, fileArgs);
|
|
1046
|
+
// Required params were registered optional because the file may
|
|
1047
|
+
// carry them (Commander validates before the file is readable) —
|
|
1048
|
+
// re-validate the merged set now. CLI-populated params are
|
|
1049
|
+
// exempt: skills_save's `files` is schema-required but filled by
|
|
1050
|
+
// the directory collector further down, which deliberately owns
|
|
1051
|
+
// the value (file-wins does not apply to collector-owned params).
|
|
1052
|
+
for (const reqParam of required) {
|
|
1053
|
+
if (tool.name === 'skills_save' && reqParam === 'files')
|
|
1054
|
+
continue;
|
|
1055
|
+
if (args[reqParam] === undefined || args[reqParam] === null) {
|
|
1056
|
+
// The primary arg is a positional — there is no --<name>
|
|
1057
|
+
// flag for it, so the recovery hint must not invent one.
|
|
1058
|
+
const hint = reqParam === tool.cli.primary_arg
|
|
1059
|
+
? `pass it as the positional argument or include it in --args-file`
|
|
1060
|
+
: `pass --${reqParam} or include it in --args-file`;
|
|
1061
|
+
logError(`Missing required parameter: ${reqParam} (${hint})`);
|
|
1062
|
+
process.exit(1);
|
|
1038
1063
|
}
|
|
1039
1064
|
}
|
|
1040
1065
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const execVal = args.execute;
|
|
1055
|
-
if (execVal !== undefined) {
|
|
1056
|
-
sbGitExecuteRequested = true;
|
|
1057
|
-
sbGitExecuteDest =
|
|
1058
|
-
typeof execVal === 'string' && execVal.length > 0
|
|
1059
|
-
? execVal
|
|
1060
|
-
: undefined;
|
|
1061
|
-
}
|
|
1062
|
-
delete args.execute;
|
|
1063
|
-
}
|
|
1064
|
-
// skills pull --out is client-side only; never forward. `pull` always
|
|
1065
|
-
// materialises to disk (default ./<slug>); --out just overrides where.
|
|
1066
|
-
let skillsPullOut;
|
|
1067
|
-
if (tool.name === 'skills_pull') {
|
|
1068
|
-
const outVal = args.out;
|
|
1069
|
-
skillsPullOut =
|
|
1070
|
-
typeof outVal === 'string' && outVal.length > 0 ? outVal : undefined;
|
|
1071
|
-
delete args.out;
|
|
1072
|
-
}
|
|
1073
|
-
// skills sync: --check/--mount-dir/--store-dir are CLI-only. The
|
|
1074
|
-
// digest `state` sent to the server is always computed here (from
|
|
1075
|
-
// the on-disk state file the materializer loop owns), never
|
|
1076
|
-
// accepted as user input.
|
|
1077
|
-
let skillsSyncCheck = false;
|
|
1078
|
-
let skillsSyncStoreDir = '';
|
|
1079
|
-
let skillsSyncMountDir = '';
|
|
1080
|
-
if (tool.name === 'skills_sync') {
|
|
1081
|
-
const { loadState, defaultStoreDir, defaultMountDir } = await import('./commands/skills-sync.js');
|
|
1082
|
-
skillsSyncCheck = !!args.check;
|
|
1083
|
-
skillsSyncStoreDir = args.storeDir
|
|
1084
|
-
? expandHome(args.storeDir)
|
|
1085
|
-
: defaultStoreDir();
|
|
1086
|
-
skillsSyncMountDir = args.mountDir
|
|
1087
|
-
? expandHome(args.mountDir)
|
|
1088
|
-
: defaultMountDir();
|
|
1089
|
-
delete args.check;
|
|
1090
|
-
delete args.storeDir;
|
|
1091
|
-
delete args.mountDir;
|
|
1092
|
-
args.state = JSON.stringify(loadState(skillsSyncStoreDir));
|
|
1093
|
-
}
|
|
1094
|
-
// skills save: read the local directory (default cwd) and ship its
|
|
1095
|
-
// files inline — the inverse of `skills pull`. Client-side because
|
|
1096
|
-
// only the local machine can walk a directory tree.
|
|
1097
|
-
if (tool.name === 'skills_save') {
|
|
1098
|
-
const { collectSkillDir } = await import('./commands/skills-save.js');
|
|
1099
|
-
const dir = pathModule.resolve(primaryArgValue || '.');
|
|
1100
|
-
let collected;
|
|
1101
|
-
try {
|
|
1102
|
-
collected = collectSkillDir(dir);
|
|
1103
|
-
}
|
|
1104
|
-
catch (e) {
|
|
1105
|
-
logError(`skills save: ${e.message}`);
|
|
1106
|
-
process.exit(1);
|
|
1066
|
+
// Coerce numeric strings to numbers based on schema
|
|
1067
|
+
for (const [name, param] of Object.entries(props)) {
|
|
1068
|
+
if (args[name] !== undefined) {
|
|
1069
|
+
const paramType = Array.isArray(param.type)
|
|
1070
|
+
? param.type[0]
|
|
1071
|
+
: param.type;
|
|
1072
|
+
if (paramType === 'integer' || paramType === 'number') {
|
|
1073
|
+
const num = Number(args[name]);
|
|
1074
|
+
if (!isNaN(num)) {
|
|
1075
|
+
args[name] = paramType === 'integer' ? Math.floor(num) : num;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1107
1079
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1080
|
+
// aidrive: strip client-side-only options before sending to API.
|
|
1081
|
+
// --local_file and --override are only meaningful to the CLI and must
|
|
1082
|
+
// never be forwarded to the server (other aidrive actions like ls,
|
|
1083
|
+
// mkdir, move don't know about them).
|
|
1084
|
+
if (tool.name === 'aidrive') {
|
|
1085
|
+
delete args.local_file;
|
|
1086
|
+
delete args.override;
|
|
1111
1087
|
}
|
|
1112
|
-
//
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// skipped) so the streaming upload below handles it; if there is still
|
|
1126
|
-
// no usable source, fail with an actionable message that names
|
|
1127
|
-
// --local_file rather than the opaque backend error.
|
|
1128
|
-
if (tool.name === 'aidrive' &&
|
|
1129
|
-
// args.action, not the positional: the action may arrive via
|
|
1130
|
-
// --args-file (sb-git's clone-url gate uses the same pattern).
|
|
1131
|
-
args.action === 'upload' &&
|
|
1132
|
-
!opts.local_file &&
|
|
1133
|
-
// args, not opts: file_content is a server param and may have
|
|
1134
|
-
// arrived via --args-file (merged into args above).
|
|
1135
|
-
!args.file_content) {
|
|
1136
|
-
// Skip the first operand only when it really is the action: with
|
|
1137
|
-
// the action arriving via --args-file, Commander binds a bare
|
|
1138
|
-
// path to the [action] positional slot, so cmd.args[0] is itself
|
|
1139
|
-
// a path candidate.
|
|
1140
|
-
const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
|
|
1141
|
-
? cmd.args.slice(1)
|
|
1142
|
-
: cmd.args;
|
|
1143
|
-
const pathOperand = pickExistingUploadOperand(operandCandidates);
|
|
1144
|
-
if (pathOperand) {
|
|
1145
|
-
opts.local_file = pathOperand;
|
|
1088
|
+
// sb-git --execute is client-side only; never forward.
|
|
1089
|
+
let sbGitExecuteDest;
|
|
1090
|
+
let sbGitExecuteRequested = false;
|
|
1091
|
+
if (tool.name === 'sb-git') {
|
|
1092
|
+
const execVal = args.execute;
|
|
1093
|
+
if (execVal !== undefined) {
|
|
1094
|
+
sbGitExecuteRequested = true;
|
|
1095
|
+
sbGitExecuteDest =
|
|
1096
|
+
typeof execVal === 'string' && execVal.length > 0
|
|
1097
|
+
? execVal
|
|
1098
|
+
: undefined;
|
|
1099
|
+
}
|
|
1100
|
+
delete args.execute;
|
|
1146
1101
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1102
|
+
// skills pull --out is client-side only; never forward. `pull` always
|
|
1103
|
+
// materialises to disk (default ./<slug>); --out just overrides where.
|
|
1104
|
+
let skillsPullOut;
|
|
1105
|
+
if (tool.name === 'skills_pull') {
|
|
1106
|
+
const outVal = args.out;
|
|
1107
|
+
skillsPullOut =
|
|
1108
|
+
typeof outVal === 'string' && outVal.length > 0 ? outVal : undefined;
|
|
1109
|
+
delete args.out;
|
|
1153
1110
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1111
|
+
// skills sync: --check/--mount-dir/--store-dir are CLI-only. The
|
|
1112
|
+
// digest `state` sent to the server is always computed here (from
|
|
1113
|
+
// the on-disk state file the materializer loop owns), never
|
|
1114
|
+
// accepted as user input.
|
|
1115
|
+
let skillsSyncCheck = false;
|
|
1116
|
+
let skillsSyncStoreDir = '';
|
|
1117
|
+
let skillsSyncMountDir = '';
|
|
1118
|
+
if (tool.name === 'skills_sync') {
|
|
1119
|
+
const { loadState, defaultStoreDir, defaultMountDir } = await import('./commands/skills-sync.js');
|
|
1120
|
+
skillsSyncCheck = !!args.check;
|
|
1121
|
+
skillsSyncStoreDir = args.storeDir
|
|
1122
|
+
? expandHome(args.storeDir)
|
|
1123
|
+
: defaultStoreDir();
|
|
1124
|
+
skillsSyncMountDir = args.mountDir
|
|
1125
|
+
? expandHome(args.mountDir)
|
|
1126
|
+
: defaultMountDir();
|
|
1127
|
+
delete args.check;
|
|
1128
|
+
delete args.storeDir;
|
|
1129
|
+
delete args.mountDir;
|
|
1130
|
+
args.state = JSON.stringify(loadState(skillsSyncStoreDir));
|
|
1167
1131
|
}
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
logError(`
|
|
1132
|
+
// skills save: read the local directory (default cwd) and ship its
|
|
1133
|
+
// files inline — the inverse of `skills pull`. Client-side because
|
|
1134
|
+
// only the local machine can walk a directory tree.
|
|
1135
|
+
if (tool.name === 'skills_save') {
|
|
1136
|
+
const { collectSkillDir } = await import('./commands/skills-save.js');
|
|
1137
|
+
const dir = pathModule.resolve(primaryArgValue || '.');
|
|
1138
|
+
let collected;
|
|
1139
|
+
try {
|
|
1140
|
+
collected = collectSkillDir(dir);
|
|
1141
|
+
}
|
|
1142
|
+
catch (e) {
|
|
1143
|
+
logError(`skills save: ${e.message}`);
|
|
1180
1144
|
process.exit(1);
|
|
1181
1145
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const destination = pathModule.posix.join(uploadPath, file.relativePath);
|
|
1186
|
-
const result = await uploadToAiDrive(file.absolutePath, destination, override, workspace);
|
|
1187
|
-
if (result.status !== 'ok') {
|
|
1188
|
-
output(result);
|
|
1189
|
-
process.exit(1);
|
|
1190
|
-
}
|
|
1191
|
-
uploaded.push(destination);
|
|
1146
|
+
if (collected.sidecarOwner) {
|
|
1147
|
+
info(`Warning: ${dir} carries provenance for ${collected.sidecarOwner} — ` +
|
|
1148
|
+
`saving will fork this skill into your own catalog.`);
|
|
1192
1149
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1150
|
+
// Collector-owned: `files` must be {path, content} read from the
|
|
1151
|
+
// local directory, so this assignment intentionally overrides any
|
|
1152
|
+
// --args-file value (the file-wins rule covers flag-shaped params
|
|
1153
|
+
// only, not CLI-populated ones).
|
|
1154
|
+
args.files = collected.files;
|
|
1198
1155
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1156
|
+
// aidrive upload ergonomics: `gsk aidrive upload <path>` is the natural
|
|
1157
|
+
// invocation and the dominant fumble — the bare path is an excess
|
|
1158
|
+
// operand that Commander tolerates but never maps to an option, so the
|
|
1159
|
+
// request otherwise reaches the API with no content and fails with a
|
|
1160
|
+
// misleading "Missing file_content". When no explicit content source was
|
|
1161
|
+
// given, adopt a path-like operand as --local_file (cmd.args[0] is the
|
|
1162
|
+
// 'upload' action; later operands are candidate paths, directories
|
|
1163
|
+
// skipped) so the streaming upload below handles it; if there is still
|
|
1164
|
+
// no usable source, fail with an actionable message that names
|
|
1165
|
+
// --local_file rather than the opaque backend error.
|
|
1166
|
+
if (tool.name === 'aidrive' &&
|
|
1167
|
+
// args.action, not the positional: the action may arrive via
|
|
1168
|
+
// --args-file (sb-git's clone-url gate uses the same pattern).
|
|
1169
|
+
args.action === 'upload' &&
|
|
1170
|
+
!opts.local_file &&
|
|
1171
|
+
// args, not opts: file_content is a server param and may have
|
|
1172
|
+
// arrived via --args-file (merged into args above).
|
|
1173
|
+
!args.file_content) {
|
|
1174
|
+
// Skip the first operand only when it really is the action: with
|
|
1175
|
+
// the action arriving via --args-file, Commander binds a bare
|
|
1176
|
+
// path to the [action] positional slot, so cmd.args[0] is itself
|
|
1177
|
+
// a path candidate.
|
|
1178
|
+
const operandCandidates = primaryArgValue !== undefined && primaryArgValue === args.action
|
|
1179
|
+
? invokedCmd.args.slice(1)
|
|
1180
|
+
: invokedCmd.args;
|
|
1181
|
+
const pathOperand = pickExistingUploadOperand(operandCandidates);
|
|
1182
|
+
if (pathOperand) {
|
|
1183
|
+
opts.local_file = pathOperand;
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
logError('upload needs a file source: use `--local_file <path>` for a ' +
|
|
1187
|
+
'local file (any size, binaries OK) or `--file_content <text>` ' +
|
|
1188
|
+
'for inline text. Example: gsk aidrive upload --local_file ' +
|
|
1189
|
+
'./report.pdf --upload_path /report.pdf');
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1203
1192
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
delete args.image;
|
|
1217
|
-
delete args.file;
|
|
1218
|
-
}
|
|
1219
|
-
// Non-Design create_task --file keeps its CSV/Excel file_urls contract.
|
|
1220
|
-
if (tool.name === 'create_task' &&
|
|
1221
|
-
args.task_type !== 'design' &&
|
|
1222
|
-
opts.file) {
|
|
1223
|
-
const fileValues = Array.isArray(opts.file)
|
|
1224
|
-
? opts.file
|
|
1225
|
-
: [opts.file];
|
|
1226
|
-
const fileUrls = [];
|
|
1227
|
-
for (const filePath of fileValues) {
|
|
1228
|
-
if (isLocalFilePath(filePath)) {
|
|
1229
|
-
// isLocalFilePath already verified existence via fs.existsSync
|
|
1230
|
-
const uploaded = await uploadLocalFile(filePath, client);
|
|
1231
|
-
const basename = pathModule.basename(filePath);
|
|
1232
|
-
const sep = uploaded.includes('?') ? '&' : '?';
|
|
1233
|
-
fileUrls.push(`${uploaded}${sep}original_name=${encodeURIComponent(basename)}`);
|
|
1193
|
+
// aidrive: handle --local_file for the upload action.
|
|
1194
|
+
// Stream the file directly to AI Drive via the dedicated tool_cli
|
|
1195
|
+
// endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
|
|
1196
|
+
if (tool.name === 'aidrive' &&
|
|
1197
|
+
args.action === 'upload' &&
|
|
1198
|
+
opts.local_file) {
|
|
1199
|
+
const localFilePath = opts.local_file;
|
|
1200
|
+
const override = !!opts.override;
|
|
1201
|
+
const workspace = args.workspace || 'personal';
|
|
1202
|
+
if (!fs.existsSync(localFilePath)) {
|
|
1203
|
+
logError(`File not found: ${localFilePath}`);
|
|
1204
|
+
process.exit(1);
|
|
1234
1205
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1206
|
+
// args.upload_path is safe here: args = { ...opts } at line above, and
|
|
1207
|
+
// only local_file/override were deleted from args — upload_path is intact.
|
|
1208
|
+
const rawUploadPath = args.upload_path ||
|
|
1209
|
+
`/${pathModule.basename(localFilePath)}`;
|
|
1210
|
+
const uploadPath = rawUploadPath.startsWith('/')
|
|
1211
|
+
? rawUploadPath
|
|
1212
|
+
: `/${rawUploadPath}`;
|
|
1213
|
+
const localStat = fs.statSync(localFilePath);
|
|
1214
|
+
const files = collectUploadFiles(localFilePath);
|
|
1215
|
+
if (localStat.isDirectory()) {
|
|
1216
|
+
if (files.length === 0) {
|
|
1217
|
+
logError(`Folder has no files to upload: ${localFilePath}`);
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
info(`Uploading folder ${localFilePath} (${files.length} files)...`);
|
|
1221
|
+
const uploaded = [];
|
|
1222
|
+
for (const file of files) {
|
|
1223
|
+
const destination = pathModule.posix.join(uploadPath, file.relativePath);
|
|
1224
|
+
const result = await uploadToAiDrive(file.absolutePath, destination, override, workspace);
|
|
1225
|
+
if (result.status !== 'ok') {
|
|
1226
|
+
output(result);
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
uploaded.push(destination);
|
|
1230
|
+
}
|
|
1231
|
+
output({
|
|
1232
|
+
status: 'ok',
|
|
1233
|
+
message: `Uploaded ${uploaded.length} files`,
|
|
1234
|
+
data: { workspace, root: uploadPath, files: uploaded },
|
|
1235
|
+
});
|
|
1239
1236
|
}
|
|
1240
1237
|
else {
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
|
|
1238
|
+
// Stream directly to AI Drive — single request, no blob middleman
|
|
1239
|
+
const result = await uploadToAiDrive(localFilePath, uploadPath, override, workspace);
|
|
1240
|
+
output(result);
|
|
1244
1241
|
}
|
|
1242
|
+
return;
|
|
1245
1243
|
}
|
|
1246
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const isSbGitCloneUrl = tool.name === 'sb-git' &&
|
|
1286
|
-
resolvedArgs.action === 'clone-url';
|
|
1287
|
-
if (isSbGitCloneUrl &&
|
|
1288
|
-
result.status === 'ok' &&
|
|
1289
|
-
result.data &&
|
|
1290
|
-
typeof result.data === 'object') {
|
|
1291
|
-
const data = result.data;
|
|
1292
|
-
if (typeof data.url === 'string') {
|
|
1293
|
-
const before = data.url;
|
|
1294
|
-
const after = normalizeCloneUrlForGit(before);
|
|
1295
|
-
data.url = after;
|
|
1296
|
-
if (Array.isArray(data.commands)) {
|
|
1297
|
-
data.commands = data.commands.map(cmd => typeof cmd === 'string' ? cmd.replace(before, after) : cmd);
|
|
1244
|
+
const designAttachmentArgs = collectDesignAttachmentArgs(process.argv);
|
|
1245
|
+
const isDesignAttachmentCommand = designAttachmentArgs.length > 0 &&
|
|
1246
|
+
((tool.name === 'create_task' && args.task_type === 'design') ||
|
|
1247
|
+
tool.name === 'task_continue');
|
|
1248
|
+
if (isDesignAttachmentCommand) {
|
|
1249
|
+
const uploaded = await uploadDesignAttachments(designAttachmentArgs, client);
|
|
1250
|
+
const existing = Array.isArray(args.attachments)
|
|
1251
|
+
? args.attachments
|
|
1252
|
+
: [];
|
|
1253
|
+
args.attachments = [...existing, ...uploaded];
|
|
1254
|
+
delete args.image;
|
|
1255
|
+
delete args.file;
|
|
1256
|
+
}
|
|
1257
|
+
// Non-Design create_task --file keeps its CSV/Excel file_urls contract.
|
|
1258
|
+
if (tool.name === 'create_task' &&
|
|
1259
|
+
args.task_type !== 'design' &&
|
|
1260
|
+
opts.file) {
|
|
1261
|
+
const fileValues = Array.isArray(opts.file)
|
|
1262
|
+
? opts.file
|
|
1263
|
+
: [opts.file];
|
|
1264
|
+
const fileUrls = [];
|
|
1265
|
+
for (const filePath of fileValues) {
|
|
1266
|
+
if (isLocalFilePath(filePath)) {
|
|
1267
|
+
// isLocalFilePath already verified existence via fs.existsSync
|
|
1268
|
+
const uploaded = await uploadLocalFile(filePath, client);
|
|
1269
|
+
const basename = pathModule.basename(filePath);
|
|
1270
|
+
const sep = uploaded.includes('?') ? '&' : '?';
|
|
1271
|
+
fileUrls.push(`${uploaded}${sep}original_name=${encodeURIComponent(basename)}`);
|
|
1272
|
+
}
|
|
1273
|
+
else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(filePath) ||
|
|
1274
|
+
filePath.startsWith('/api/')) {
|
|
1275
|
+
// Matches any URI scheme (http://, https://, aidrive://, etc.) or /api/ paths
|
|
1276
|
+
fileUrls.push(filePath);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
// Not a URL and not an existing local file — likely a typo
|
|
1280
|
+
logError(`File not found: ${filePath}`);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1298
1283
|
}
|
|
1284
|
+
// Merge with any --file_urls passed directly
|
|
1285
|
+
const existing = Array.isArray(args.file_urls)
|
|
1286
|
+
? args.file_urls
|
|
1287
|
+
: [];
|
|
1288
|
+
args.file_urls = [...existing, ...fileUrls];
|
|
1289
|
+
delete args.file; // CLI-only option, not a server parameter
|
|
1299
1290
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
result
|
|
1308
|
-
result.data
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
// skills sync: `--check` reports a diff without writing. Otherwise
|
|
1341
|
-
// materialize the response via applySyncResponse (Task 5), looping
|
|
1342
|
-
// while the server signals a truncated bundle (`complete: false`) —
|
|
1343
|
-
// only the local machine holds the digest state across calls and
|
|
1344
|
-
// can write the resulting file tree / symlinks.
|
|
1345
|
-
if (tool.name === 'skills_sync' &&
|
|
1346
|
-
result.status === 'ok' &&
|
|
1347
|
-
result.data &&
|
|
1348
|
-
typeof result.data === 'object') {
|
|
1349
|
-
if (skillsSyncCheck) {
|
|
1291
|
+
// Resolve local file paths to uploaded URLs
|
|
1292
|
+
const resolvedArgs = await resolveLocalFiles(args, client);
|
|
1293
|
+
// The server owns the async-first default: create_task /
|
|
1294
|
+
// task_continue return a submit stub ({status: "submitted",
|
|
1295
|
+
// run_id, stream_url}) unless `--wait true` was passed. --attach
|
|
1296
|
+
// upgrades the stub into a live SSE follow that exits with the
|
|
1297
|
+
// run's terminal result — the work still executes server-side.
|
|
1298
|
+
let result = await client.executeTool(tool.name, resolvedArgs);
|
|
1299
|
+
const stubData = result.data;
|
|
1300
|
+
if (attachTask &&
|
|
1301
|
+
result.status === 'ok' &&
|
|
1302
|
+
stubData &&
|
|
1303
|
+
typeof stubData.stream_url === 'string' &&
|
|
1304
|
+
(stubData.status === 'submitted' ||
|
|
1305
|
+
stubData.status === 'duplicate_submit_converged')) {
|
|
1306
|
+
const meta = await client.attachGskTaskRun(stubData.stream_url);
|
|
1307
|
+
const runStatus = String(meta.status || '');
|
|
1308
|
+
const summary = meta.result_summary && typeof meta.result_summary === 'object'
|
|
1309
|
+
? meta.result_summary
|
|
1310
|
+
: {};
|
|
1311
|
+
result = {
|
|
1312
|
+
status: runStatus === 'succeeded' ? 'ok' : 'error',
|
|
1313
|
+
message: runStatus === 'succeeded'
|
|
1314
|
+
? 'success'
|
|
1315
|
+
: `task ended with status ${runStatus || 'unknown'}`,
|
|
1316
|
+
data: {
|
|
1317
|
+
...summary,
|
|
1318
|
+
run_id: stubData.run_id,
|
|
1319
|
+
run_status: runStatus,
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
const isSbGitCloneUrl = tool.name === 'sb-git' &&
|
|
1324
|
+
resolvedArgs.action === 'clone-url';
|
|
1325
|
+
if (isSbGitCloneUrl &&
|
|
1326
|
+
result.status === 'ok' &&
|
|
1327
|
+
result.data &&
|
|
1328
|
+
typeof result.data === 'object') {
|
|
1350
1329
|
const data = result.data;
|
|
1351
|
-
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1330
|
+
if (typeof data.url === 'string') {
|
|
1331
|
+
const before = data.url;
|
|
1332
|
+
const after = normalizeCloneUrlForGit(before);
|
|
1333
|
+
data.url = after;
|
|
1334
|
+
if (Array.isArray(data.commands)) {
|
|
1335
|
+
data.commands = data.commands.map(cmd => typeof cmd === 'string' ? cmd.replace(before, after) : cmd);
|
|
1336
|
+
}
|
|
1354
1337
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1338
|
+
}
|
|
1339
|
+
// sb-git clone-url --execute: substitute placeholders + spawn
|
|
1340
|
+
// `git clone` locally. Backend response stays placeholder-only
|
|
1341
|
+
// (no secrets in stdout / logs); substitution + subprocess live
|
|
1342
|
+
// here because they need in-process auth + access to git.
|
|
1343
|
+
if (isSbGitCloneUrl &&
|
|
1344
|
+
sbGitExecuteRequested &&
|
|
1345
|
+
result.status === 'ok' &&
|
|
1346
|
+
result.data &&
|
|
1347
|
+
typeof result.data === 'object') {
|
|
1348
|
+
const globalOpts = getGlobalOptions();
|
|
1349
|
+
const code = await executeCloneUrl({
|
|
1350
|
+
data: result.data,
|
|
1351
|
+
apiKey: globalOpts.apiKey || '',
|
|
1352
|
+
baseUrl: globalOpts.baseUrl,
|
|
1353
|
+
explicitDest: sbGitExecuteDest,
|
|
1354
|
+
});
|
|
1355
|
+
if (code !== 0)
|
|
1356
|
+
process.exit(code);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
// skills pull: decode the returned file rows + write the skill tree to
|
|
1360
|
+
// disk (default ./<slug>, or --out <dir>). Client-side because the
|
|
1361
|
+
// backend returns JSON, not a filesystem. Pull always materialises;
|
|
1362
|
+
// the base64 blobs are dropped from the echoed result below.
|
|
1363
|
+
if (tool.name === 'skills_pull' &&
|
|
1364
|
+
result.status === 'ok' &&
|
|
1365
|
+
result.data &&
|
|
1366
|
+
typeof result.data === 'object') {
|
|
1367
|
+
const { writeSkillFiles } = await import('./commands/skills-pull.js');
|
|
1368
|
+
const code = writeSkillFiles(result.data, skillsPullOut);
|
|
1369
|
+
if (code !== 0)
|
|
1370
|
+
process.exit(code);
|
|
1371
|
+
// Drop the (large, base64) file blobs from the echoed result — the
|
|
1372
|
+
// user asked to write to disk, not to dump bytes to stdout.
|
|
1373
|
+
const data = result.data;
|
|
1374
|
+
delete data.files;
|
|
1358
1375
|
output(result);
|
|
1359
|
-
if (dirty)
|
|
1360
|
-
process.exit(3);
|
|
1361
1376
|
return;
|
|
1362
1377
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1378
|
+
// skills sync: `--check` reports a diff without writing. Otherwise
|
|
1379
|
+
// materialize the response via applySyncResponse (Task 5), looping
|
|
1380
|
+
// while the server signals a truncated bundle (`complete: false`) —
|
|
1381
|
+
// only the local machine holds the digest state across calls and
|
|
1382
|
+
// can write the resulting file tree / symlinks.
|
|
1383
|
+
if (tool.name === 'skills_sync' &&
|
|
1384
|
+
result.status === 'ok' &&
|
|
1385
|
+
result.data &&
|
|
1386
|
+
typeof result.data === 'object') {
|
|
1387
|
+
if (skillsSyncCheck) {
|
|
1388
|
+
const data = result.data;
|
|
1389
|
+
for (const entry of data.skills) {
|
|
1390
|
+
const suffix = entry.code ? ` (${entry.code})` : '';
|
|
1391
|
+
info(`${entry.slug}: ${entry.action}${suffix}`);
|
|
1392
|
+
}
|
|
1393
|
+
const dirty = data.skills.some(entry => entry.action !== 'unchanged');
|
|
1394
|
+
for (const entry of data.skills)
|
|
1395
|
+
delete entry.files;
|
|
1396
|
+
output(result);
|
|
1397
|
+
if (dirty)
|
|
1398
|
+
process.exit(3);
|
|
1399
|
+
return;
|
|
1384
1400
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1401
|
+
const { applySyncResponse, SyncAggregator } = await import('./commands/skills-sync.js');
|
|
1402
|
+
const syncOpts = {
|
|
1403
|
+
storeDir: skillsSyncStoreDir,
|
|
1404
|
+
mountDir: skillsSyncMountDir,
|
|
1405
|
+
};
|
|
1406
|
+
let current = result;
|
|
1407
|
+
// Fold every batch's outcome into deduped totals (see SyncAggregator):
|
|
1408
|
+
// a server `error` row re-appears each batch, so concatenating would
|
|
1409
|
+
// multi-count it and the summary + exit code would lie.
|
|
1410
|
+
const agg = new SyncAggregator();
|
|
1411
|
+
let iterations = 0;
|
|
1412
|
+
for (;;) {
|
|
1413
|
+
const data = current.data;
|
|
1414
|
+
const applyResult = applySyncResponse(data, syncOpts);
|
|
1415
|
+
agg.add(applyResult, current.data.skills);
|
|
1416
|
+
if (data.complete !== false)
|
|
1417
|
+
break;
|
|
1418
|
+
iterations += 1;
|
|
1419
|
+
if (iterations >= 20) {
|
|
1420
|
+
logError('skills sync: exceeded 20 batches without completing — aborting');
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
current = await client.executeTool('skills_sync', {
|
|
1424
|
+
state: JSON.stringify(applyResult.state),
|
|
1425
|
+
});
|
|
1426
|
+
if (current.status !== 'ok' || !current.data) {
|
|
1427
|
+
logError(`skills sync: batch ${iterations} failed — ${current.message}`);
|
|
1428
|
+
process.exit(1);
|
|
1429
|
+
}
|
|
1391
1430
|
}
|
|
1431
|
+
info(`synced ${agg.appliedCount} updated / ${agg.removedCount} removed / ${agg.failedCount} failed`);
|
|
1432
|
+
// Emit the aggregated (all-batch, deduped) skill rows rather than only
|
|
1433
|
+
// the final batch's, stripping the base64 file blobs first.
|
|
1434
|
+
const aggregatedSkills = agg.skills;
|
|
1435
|
+
for (const entry of aggregatedSkills)
|
|
1436
|
+
delete entry.files;
|
|
1437
|
+
current.data.skills = aggregatedSkills;
|
|
1438
|
+
output(current);
|
|
1439
|
+
// A boot script runs `sync` every startup and keys off the exit
|
|
1440
|
+
// code; a per-slug failure (mount refused, write error, server
|
|
1441
|
+
// `error` row) must not read as success. Exit 4 (distinct from 3 =
|
|
1442
|
+
// `--check` found a diff, and 1 = the sync loop itself aborted) so
|
|
1443
|
+
// the caller can tell "some skills didn't materialize" from a clean
|
|
1444
|
+
// run. Exit 0 stays reserved for fully-clean.
|
|
1445
|
+
if (agg.failedCount > 0)
|
|
1446
|
+
process.exit(4);
|
|
1447
|
+
return;
|
|
1392
1448
|
}
|
|
1393
|
-
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
// If -o was specified and tool succeeded, download the output file.
|
|
1412
|
-
// Skip for create_task — it has its own export logic below.
|
|
1413
|
-
if (tool.name !== 'create_task' &&
|
|
1414
|
-
outputFilePath &&
|
|
1415
|
-
outputFileKeys.length > 0 &&
|
|
1416
|
-
result.status === 'ok' &&
|
|
1417
|
-
result.data &&
|
|
1418
|
-
typeof result.data === 'object') {
|
|
1419
|
-
const fileUrl = extractFileUrl(result.data, outputFileKeys);
|
|
1420
|
-
if (fileUrl) {
|
|
1421
|
-
const localPath = await downloadToFile(fileUrl, outputFilePath, client);
|
|
1422
|
-
result.data.local_path = localPath;
|
|
1423
|
-
}
|
|
1424
|
-
else {
|
|
1425
|
-
info('Warning: Could not find a file URL in the result to download');
|
|
1449
|
+
// If -o was specified and tool succeeded, download the output file.
|
|
1450
|
+
// Skip for create_task — it has its own export logic below.
|
|
1451
|
+
if (tool.name !== 'create_task' &&
|
|
1452
|
+
outputFilePath &&
|
|
1453
|
+
outputFileKeys.length > 0 &&
|
|
1454
|
+
result.status === 'ok' &&
|
|
1455
|
+
result.data &&
|
|
1456
|
+
typeof result.data === 'object') {
|
|
1457
|
+
const fileUrl = extractFileUrl(result.data, outputFileKeys);
|
|
1458
|
+
if (fileUrl) {
|
|
1459
|
+
const localPath = await downloadToFile(fileUrl, outputFilePath, client);
|
|
1460
|
+
result.data.local_path = localPath;
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
info('Warning: Could not find a file URL in the result to download');
|
|
1464
|
+
}
|
|
1426
1465
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1466
|
+
// create_task -o: export artifact (pptx/docx/xlsx) after task completes
|
|
1467
|
+
if (tool.name === 'create_task' &&
|
|
1468
|
+
outputFilePath &&
|
|
1469
|
+
result.status === 'ok' &&
|
|
1470
|
+
result.data &&
|
|
1471
|
+
typeof result.data === 'object') {
|
|
1472
|
+
const data = result.data;
|
|
1473
|
+
const projectId = data.project_id;
|
|
1474
|
+
// args.type is the value actually sent to the API: the positional
|
|
1475
|
+
// is copied into args and --args-file may override it (file wins),
|
|
1476
|
+
// so the export must key on the merged view, never the positional.
|
|
1477
|
+
const taskType = args.type;
|
|
1478
|
+
if (projectId && taskType) {
|
|
1479
|
+
info(`Exporting ${taskType} artifact...`);
|
|
1480
|
+
try {
|
|
1481
|
+
const exportResult = await client.exportArtifact(projectId, taskType);
|
|
1482
|
+
if (exportResult.status === 'ok' && exportResult.data) {
|
|
1483
|
+
const expData = exportResult.data;
|
|
1484
|
+
const downloadUrl = expData.download_url;
|
|
1485
|
+
if (downloadUrl) {
|
|
1486
|
+
const localPath = await downloadToFile(downloadUrl, outputFilePath, client);
|
|
1487
|
+
data.local_path = localPath;
|
|
1488
|
+
data.export_format = expData.format;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
info(`Warning: Export not available — ${exportResult.message || 'unknown error'}`);
|
|
1451
1493
|
}
|
|
1452
1494
|
}
|
|
1453
|
-
|
|
1454
|
-
info(`Warning: Export
|
|
1495
|
+
catch (exportErr) {
|
|
1496
|
+
info(`Warning: Export failed — ${exportErr.message}`);
|
|
1455
1497
|
}
|
|
1456
1498
|
}
|
|
1457
|
-
catch (exportErr) {
|
|
1458
|
-
info(`Warning: Export failed — ${exportErr.message}`);
|
|
1459
|
-
}
|
|
1460
1499
|
}
|
|
1500
|
+
// Media generation payloads print compact + a stderr field summary
|
|
1501
|
+
// so agent-side `grep | head` filters can't lose the URLs/task_id
|
|
1502
|
+
// (the fabricated-URL incident behind PR #41690) — see
|
|
1503
|
+
// mediaSummary.ts for the full rationale.
|
|
1504
|
+
for (const line of mediaSummaryLines(tool.name, result)) {
|
|
1505
|
+
info(line);
|
|
1506
|
+
}
|
|
1507
|
+
// web_search payloads are large and agent-consumed — indent-2 here
|
|
1508
|
+
// is pure token overhead on every SAS/Claw search
|
|
1509
|
+
output(result, {
|
|
1510
|
+
compact: tool.name === 'web_search' || Boolean(mediaItemsKeyFor(tool.name)),
|
|
1511
|
+
});
|
|
1461
1512
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
// mediaSummary.ts for the full rationale.
|
|
1466
|
-
for (const line of mediaSummaryLines(tool.name, result)) {
|
|
1467
|
-
info(line);
|
|
1513
|
+
catch (err) {
|
|
1514
|
+
logError(err.message);
|
|
1515
|
+
process.exit(1);
|
|
1468
1516
|
}
|
|
1469
|
-
// web_search payloads are large and agent-consumed — indent-2 here
|
|
1470
|
-
// is pure token overhead on every SAS/Claw search
|
|
1471
|
-
output(result, {
|
|
1472
|
-
compact: tool.name === 'web_search' || Boolean(mediaItemsKeyFor(tool.name)),
|
|
1473
|
-
});
|
|
1474
1517
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1518
|
+
};
|
|
1519
|
+
cmd.action((primaryArgValue, opts) => runToolInvocation(primaryArgValue, opts));
|
|
1520
|
+
// Per-action subcommands (issue #51377) — see the actionViews comment at
|
|
1521
|
+
// the top of this function: a wrong-action flag now fails at parse time
|
|
1522
|
+
// with a did-you-mean, and `gsk teams send --help` shows only that
|
|
1523
|
+
// action's flags, while the flag-free parent still handles the bare
|
|
1524
|
+
// invocation (action listing), --args-file carrying the action, and
|
|
1525
|
+
// unknown action names (surfaced server-side).
|
|
1526
|
+
if (actionViews) {
|
|
1527
|
+
const allActionNames = actionViews.map(view => view.name);
|
|
1528
|
+
for (const view of actionViews) {
|
|
1529
|
+
const sub = cmd.command(view.name).description(view.description);
|
|
1530
|
+
for (const paramName of view.parameters) {
|
|
1531
|
+
addSchemaOption(sub, paramName, props[paramName], {
|
|
1532
|
+
// Same relaxation as the union path: with --args-file on the
|
|
1533
|
+
// command line, required params may live in the file, which
|
|
1534
|
+
// Commander cannot see at parse time.
|
|
1535
|
+
isRequired: argsFileInvoked
|
|
1536
|
+
? false
|
|
1537
|
+
: view.required.includes(paramName),
|
|
1538
|
+
shortAlias: aliases[paramName],
|
|
1539
|
+
injectedDefault: tool.cli.group === 'vd' && paramName === 'vd_project_id'
|
|
1540
|
+
? vdProjectIdDefault
|
|
1541
|
+
: undefined,
|
|
1542
|
+
description: actionScopedDescription(props[paramName].description || paramName, view.name, allActionNames),
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
if (hasAttachmentsParam && view.parameters.includes('attachments')) {
|
|
1546
|
+
sub.option('--attach <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
|
|
1547
|
+
sub.option('--attachment <values...>', 'Attach local files or URLs (alias for --attachments; repeatable)');
|
|
1548
|
+
}
|
|
1549
|
+
sub.option('--args-file <path>', 'Read arguments from a JSON object file ("-" = stdin). File keys ' +
|
|
1550
|
+
'override flag values. Use for rich text (strings containing $, ' +
|
|
1551
|
+
'quotes, newlines) instead of double-quoted flags. Server ' +
|
|
1552
|
+
'parameters only — CLI-only options must still be passed as flags.');
|
|
1553
|
+
if (outputFileKeys.length > 0) {
|
|
1554
|
+
sub.option('-o, --output-file <path>', 'Download the generated file to a local path');
|
|
1555
|
+
}
|
|
1556
|
+
sub.action((opts) => runToolInvocation(view.name, opts, sub));
|
|
1478
1557
|
}
|
|
1479
|
-
}
|
|
1558
|
+
}
|
|
1480
1559
|
}
|
|
1481
1560
|
// ============================================
|
|
1482
1561
|
// Built-in Commands
|