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