diff --git a/cmd/internal/options.go b/cmd/internal/options.go index 337e2d43912f..abdc67347283 100644 --- a/cmd/internal/options.go +++ b/cmd/internal/options.go @@ -44,6 +44,7 @@ type ToolboxOptions struct { Configs []string ConfigFolder string PrebuiltConfigs []string + VersionNum string } // Option defines a function that modifies the ToolboxOptions struct. diff --git a/cmd/internal/skills/command.go b/cmd/internal/skills/command.go index 4de9c21b228b..085012fdf76e 100644 --- a/cmd/internal/skills/command.go +++ b/cmd/internal/skills/command.go @@ -40,6 +40,8 @@ type skillsCmd struct { outputDir string licenseHeader string additionalNotes string + invocationMode string + toolboxVersion string } // NewCommand creates a new Command. @@ -62,6 +64,8 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command { flags.StringVar(&cmd.outputDir, "output-dir", "skills", "Directory to output generated skills") flags.StringVar(&cmd.licenseHeader, "license-header", "", "Optional license header to prepend to generated node scripts.") flags.StringVar(&cmd.additionalNotes, "additional-notes", "", "Additional notes to add under the Usage section of the generated SKILL.md") + flags.StringVar(&cmd.invocationMode, "invocation-mode", "binary", "Invocation mode for the generated scripts: 'binary' or 'npx'") + flags.StringVar(&cmd.toolboxVersion, "toolbox-version", opts.VersionNum, "Version of @toolbox-sdk/server to use for npx approach") _ = cmd.MarkFlagRequired("name") _ = cmd.MarkFlagRequired("description") return cmd.Command @@ -187,7 +191,7 @@ func run(cmd *skillsCmd, opts *internal.ToolboxOptions) error { for _, toolName := range toolNames { // Generate wrapper script in scripts directory - scriptContent, err := generateScriptContent(toolName, configArgsStr, cmd.licenseHeader) + scriptContent, err := generateScriptContent(toolName, configArgsStr, cmd.licenseHeader, cmd.invocationMode, cmd.toolboxVersion) if err != nil { errMsg := fmt.Errorf("error generating script content for %s: %w", toolName, err) opts.Logger.ErrorContext(ctx, errMsg.Error()) diff --git a/cmd/internal/skills/generator.go b/cmd/internal/skills/generator.go index a301a18a6ee6..014b7cbddb1e 100644 --- a/cmd/internal/skills/generator.go +++ b/cmd/internal/skills/generator.go @@ -124,38 +124,11 @@ const nodeScriptTemplate = `#!/usr/bin/env node const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); +const os = require('os'); const toolName = "{{.Name}}"; const configArgs = [{{.ConfigArgs}}]; -function getToolboxPath() { - if (process.env.GEMINI_CLI === '1') { - const ext = process.platform === 'win32' ? '.exe' : ''; - const localPath = path.resolve(__dirname, '../../../toolbox' + ext); - if (fs.existsSync(localPath)) { - return localPath; - } - } - try { - const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox'; - const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim(); - if (globalPath) { - return globalPath.split('\n')[0].trim(); - } - throw new Error("Toolbox binary not found"); - } catch (e) { - throw new Error("Toolbox binary not found"); - } -} - -let toolboxBinary; -try { - toolboxBinary = getToolboxPath(); -} catch (err) { - console.error("Error:", err.message); - process.exit(1); -} - function getEnv() { const envPath = path.resolve(__dirname, '../../../.env'); const env = { ...process.env }; @@ -188,9 +161,47 @@ if (process.env.GEMINI_CLI === '1') { const args = process.argv.slice(2); +{{if eq .InvocationMode "npx"}} +const command = os.platform() === 'win32' ? 'npx.cmd' : 'npx'; + +const processedArgs = os.platform() === 'win32' ? args.map(arg => arg.includes('"') ? '"' + arg.replace(/"/g, '""') + '"' : arg) : args; + +const npxArgs = ["--yes", "@toolbox-sdk/server@{{.ToolboxVersion}}", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs]; + +const child = spawn(command, npxArgs, { shell: os.platform() === 'win32', stdio: 'inherit', env }); +{{else}} +function getToolboxPath() { + if (process.env.GEMINI_CLI === '1') { + const ext = process.platform === 'win32' ? '.exe' : ''; + const localPath = path.resolve(__dirname, '../../../toolbox' + ext); + if (fs.existsSync(localPath)) { + return localPath; + } + } + try { + const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox'; + const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim(); + if (globalPath) { + return globalPath.split('\n')[0].trim(); + } + throw new Error("Toolbox binary not found"); + } catch (e) { + throw new Error("Toolbox binary not found"); + } +} + +let toolboxBinary; +try { + toolboxBinary = getToolboxPath(); +} catch (err) { + console.error("Error:", err.message); + process.exit(1); +} + const toolboxArgs = ["--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit', env }); +{{end}} child.on('close', (code) => { process.exit(code); @@ -203,19 +214,23 @@ child.on('error', (err) => { ` type scriptData struct { - Name string - ConfigArgs string - LicenseHeader string + Name string + ConfigArgs string + LicenseHeader string + InvocationMode string + ToolboxVersion string } // generateScriptContent creates the content for a Node.js wrapper script. // This script invokes the toolbox CLI with the appropriate configuration // (using a generated config) and arguments to execute the specific tool. -func generateScriptContent(name string, configArgs string, licenseHeader string) (string, error) { +func generateScriptContent(name string, configArgs string, licenseHeader string, mode string, version string) (string, error) { data := scriptData{ - Name: name, - ConfigArgs: configArgs, - LicenseHeader: licenseHeader, + Name: name, + ConfigArgs: configArgs, + LicenseHeader: licenseHeader, + InvocationMode: mode, + ToolboxVersion: version, } tmpl, err := template.New("script").Parse(nodeScriptTemplate) diff --git a/cmd/internal/skills/generator_test.go b/cmd/internal/skills/generator_test.go index 68ad0ef907ae..18cfecf1c9d0 100644 --- a/cmd/internal/skills/generator_test.go +++ b/cmd/internal/skills/generator_test.go @@ -219,11 +219,14 @@ func TestGenerateScriptContent(t *testing.T) { configArgs string wantContains []string licenseHeader string + mode string + version string }{ { - name: "basic script", + name: "basic script (binary default)", toolName: "test-tool", configArgs: `"--prebuilt", "test"`, + mode: "binary", wantContains: []string{ `const toolName = "test-tool";`, `const configArgs = ["--prebuilt", "test"];`, @@ -244,15 +247,27 @@ func TestGenerateScriptContent(t *testing.T) { toolName: "test-tool", configArgs: `"--prebuilt", "test"`, licenseHeader: "// My License", + mode: "binary", wantContains: []string{ "// My License", }, }, + { + name: "npx mode script", + toolName: "npx-tool", + configArgs: `"--prebuilt", "test"`, + mode: "npx", + version: "0.31.0", + wantContains: []string{ + `const toolName = "npx-tool";`, + `const npxArgs = ["--yes", "@toolbox-sdk/server@0.31.0"`, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := generateScriptContent(tt.toolName, tt.configArgs, tt.licenseHeader) + got, err := generateScriptContent(tt.toolName, tt.configArgs, tt.licenseHeader, tt.mode, tt.version) if err != nil { t.Fatalf("generateScriptContent() error = %v", err) } diff --git a/cmd/root.go b/cmd/root.go index 85f768cd94f3..41de25b23f40 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -105,6 +105,7 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command { // Set server version opts.Cfg.Version = versionString + opts.VersionNum = strings.TrimSpace(versionNum) // set baseCmd in, out and err the same as cmd. cmd.SetIn(opts.IOStreams.In) diff --git a/docs/en/documentation/configuration/skills/_index.md b/docs/en/documentation/configuration/skills/_index.md index aea2635f8dc5..2360b4d98635 100644 --- a/docs/en/documentation/configuration/skills/_index.md +++ b/docs/en/documentation/configuration/skills/_index.md @@ -39,6 +39,8 @@ toolbox skills-generate \ - `--output-dir`: (Optional) Directory to output generated skills (default: "skills"). - `--license-header`: (Optional) Optional license header to prepend to generated node scripts. - `--additional-notes`: (Optional) Additional notes to add under the Usage section of the generated SKILL.md. +- `--invocation-mode`: (Optional) Invocation mode for the generated scripts: 'binary' or 'npx' (default: "binary"). +- `--toolbox-version`: (Optional) Version of @toolbox-sdk/server to use for npx approach (defaults to current toolbox version). {{< notice note >}} **Note:** The `` must follow the Agent Skill [naming convention](https://agentskills.io/specification): it must contain only lowercase alphanumeric characters and hyphens, cannot start or end with a hyphen, and cannot contain consecutive hyphens (e.g., `my-skill`, `data-processing`). diff --git a/docs/en/reference/cli.md b/docs/en/reference/cli.md index f9bf2065ed86..fad7ebaa63f8 100644 --- a/docs/en/reference/cli.md +++ b/docs/en/reference/cli.md @@ -73,6 +73,8 @@ toolbox skills-generate --name --description --toolset