From f23a41f1b0e1460922972881c50de0ab4a07c5da Mon Sep 17 00:00:00 2001 From: Haoyu Wang Date: Tue, 31 Mar 2026 17:39:13 -0400 Subject: [PATCH 1/4] feat(skill): tool invocation via npx --- cmd/internal/skills/generator.go | 50 +++++++++++++------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/cmd/internal/skills/generator.go b/cmd/internal/skills/generator.go index 801101b8d576..5f1082a3d5a3 100644 --- a/cmd/internal/skills/generator.go +++ b/cmd/internal/skills/generator.go @@ -124,37 +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 localPath = path.resolve(__dirname, '../../../toolbox'); - 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 }; @@ -186,10 +160,26 @@ if (process.env.GEMINI_CLI === '1') { } const args = process.argv.slice(2); +const npxArgs = ["--yes", "@toolbox-sdk/server", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; + +let command = 'npx'; +let spawnArgs = npxArgs; + +// The Windows Dependency-Free Bypass +if (os.platform() === 'win32') { + const nodeDir = path.dirname(process.execPath); + const npxCliJs = path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npx-cli.js'); + + if (fs.existsSync(npxCliJs)) { + command = process.execPath; + spawnArgs = [npxCliJs, ...npxArgs]; + } else { + console.error("Error: Could not find the npx executable to launch."); + process.exit(1); + } +} -const toolboxArgs = ["--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; - -const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit', env }); +const child = spawn(command, spawnArgs, { stdio: 'inherit', env }); child.on('close', (code) => { process.exit(code); From d58ff453bf966cfd44fd43e056573424686b113b Mon Sep 17 00:00:00 2001 From: Haoyu Wang Date: Thu, 2 Apr 2026 10:43:43 -0400 Subject: [PATCH 2/4] make tool invocation approach configurable --- cmd/internal/skills/command.go | 4 +- cmd/internal/skills/generator.go | 51 ++++++++++++++++--- cmd/internal/skills/generator_test.go | 17 ++++++- .../configuration/skills/_index.md | 1 + docs/en/reference/cli.md | 1 + 5 files changed, 63 insertions(+), 11 deletions(-) diff --git a/cmd/internal/skills/command.go b/cmd/internal/skills/command.go index 4de9c21b228b..223f7b64e16e 100644 --- a/cmd/internal/skills/command.go +++ b/cmd/internal/skills/command.go @@ -40,6 +40,7 @@ type skillsCmd struct { outputDir string licenseHeader string additionalNotes string + invocationMode string } // NewCommand creates a new Command. @@ -62,6 +63,7 @@ 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'") _ = cmd.MarkFlagRequired("name") _ = cmd.MarkFlagRequired("description") return cmd.Command @@ -187,7 +189,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) 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 5f1082a3d5a3..ee5b59c328cb 100644 --- a/cmd/internal/skills/generator.go +++ b/cmd/internal/skills/generator.go @@ -160,12 +160,13 @@ if (process.env.GEMINI_CLI === '1') { } const args = process.argv.slice(2); + +{{if eq .InvocationMode "npx"}} const npxArgs = ["--yes", "@toolbox-sdk/server", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; let command = 'npx'; let spawnArgs = npxArgs; -// The Windows Dependency-Free Bypass if (os.platform() === 'win32') { const nodeDir = path.dirname(process.execPath); const npxCliJs = path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npx-cli.js'); @@ -180,6 +181,38 @@ if (os.platform() === 'win32') { } const child = spawn(command, spawnArgs, { stdio: 'inherit', env }); +{{else}} +function getToolboxPath() { + if (process.env.GEMINI_CLI === '1') { + const localPath = path.resolve(__dirname, '../../../toolbox'); + 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); @@ -192,19 +225,21 @@ child.on('error', (err) => { ` type scriptData struct { - Name string - ConfigArgs string - LicenseHeader string + Name string + ConfigArgs string + LicenseHeader string + InvocationMode 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) (string, error) { data := scriptData{ - Name: name, - ConfigArgs: configArgs, - LicenseHeader: licenseHeader, + Name: name, + ConfigArgs: configArgs, + LicenseHeader: licenseHeader, + InvocationMode: mode, } 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..9e123f03f8c8 100644 --- a/cmd/internal/skills/generator_test.go +++ b/cmd/internal/skills/generator_test.go @@ -219,11 +219,13 @@ func TestGenerateScriptContent(t *testing.T) { configArgs string wantContains []string licenseHeader string + mode 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 +246,26 @@ 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", + wantContains: []string{ + `const toolName = "npx-tool";`, + `const npxArgs = ["--yes", "@toolbox-sdk/server"`, + }, + }, } 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) if err != nil { t.Fatalf("generateScriptContent() error = %v", err) } diff --git a/docs/en/documentation/configuration/skills/_index.md b/docs/en/documentation/configuration/skills/_index.md index aea2635f8dc5..8496fa43ab5c 100644 --- a/docs/en/documentation/configuration/skills/_index.md +++ b/docs/en/documentation/configuration/skills/_index.md @@ -39,6 +39,7 @@ 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"). {{< 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..f5c772f74bf6 100644 --- a/docs/en/reference/cli.md +++ b/docs/en/reference/cli.md @@ -73,6 +73,7 @@ toolbox skills-generate --name --description --toolset Date: Thu, 2 Apr 2026 11:59:43 -0400 Subject: [PATCH 3/4] update npx --- cmd/internal/skills/generator.go | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/cmd/internal/skills/generator.go b/cmd/internal/skills/generator.go index ee5b59c328cb..6abcffb19db9 100644 --- a/cmd/internal/skills/generator.go +++ b/cmd/internal/skills/generator.go @@ -162,25 +162,13 @@ if (process.env.GEMINI_CLI === '1') { const args = process.argv.slice(2); {{if eq .InvocationMode "npx"}} -const npxArgs = ["--yes", "@toolbox-sdk/server", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; +const command = os.platform() === 'win32' ? 'npx.cmd' : 'npx'; -let command = 'npx'; -let spawnArgs = npxArgs; +const processedArgs = os.platform() === 'win32' ? args.map(arg => arg.includes('"') ? '"' + arg.replace(/"/g, '""') + '"' : arg) : args; -if (os.platform() === 'win32') { - const nodeDir = path.dirname(process.execPath); - const npxCliJs = path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npx-cli.js'); +const npxArgs = ["--yes", "@toolbox-sdk/server", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs]; - if (fs.existsSync(npxCliJs)) { - command = process.execPath; - spawnArgs = [npxCliJs, ...npxArgs]; - } else { - console.error("Error: Could not find the npx executable to launch."); - process.exit(1); - } -} - -const child = spawn(command, spawnArgs, { stdio: 'inherit', env }); +const child = spawn(command, npxArgs, { shell: os.platform() === 'win32', stdio: 'inherit', env }); {{else}} function getToolboxPath() { if (process.env.GEMINI_CLI === '1') { From 959c316ffc9973a6b683bf9d6a113f55857f72bd Mon Sep 17 00:00:00 2001 From: Haoyu Wang Date: Thu, 2 Apr 2026 12:48:39 -0400 Subject: [PATCH 4/4] make toolbox version configurable --- cmd/internal/options.go | 1 + cmd/internal/skills/command.go | 4 +++- cmd/internal/skills/generator.go | 6 ++++-- cmd/internal/skills/generator_test.go | 6 ++++-- cmd/root.go | 1 + docs/en/documentation/configuration/skills/_index.md | 1 + docs/en/reference/cli.md | 1 + 7 files changed, 15 insertions(+), 5 deletions(-) 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 223f7b64e16e..085012fdf76e 100644 --- a/cmd/internal/skills/command.go +++ b/cmd/internal/skills/command.go @@ -41,6 +41,7 @@ type skillsCmd struct { licenseHeader string additionalNotes string invocationMode string + toolboxVersion string } // NewCommand creates a new Command. @@ -64,6 +65,7 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command { 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 @@ -189,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, cmd.invocationMode) + 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 8ad96365a657..014b7cbddb1e 100644 --- a/cmd/internal/skills/generator.go +++ b/cmd/internal/skills/generator.go @@ -166,7 +166,7 @@ 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", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs]; +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}} @@ -218,17 +218,19 @@ type scriptData struct { 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, mode 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, 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 9e123f03f8c8..18cfecf1c9d0 100644 --- a/cmd/internal/skills/generator_test.go +++ b/cmd/internal/skills/generator_test.go @@ -220,6 +220,7 @@ func TestGenerateScriptContent(t *testing.T) { wantContains []string licenseHeader string mode string + version string }{ { name: "basic script (binary default)", @@ -256,16 +257,17 @@ func TestGenerateScriptContent(t *testing.T) { toolName: "npx-tool", configArgs: `"--prebuilt", "test"`, mode: "npx", + version: "0.31.0", wantContains: []string{ `const toolName = "npx-tool";`, - `const npxArgs = ["--yes", "@toolbox-sdk/server"`, + `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, tt.mode) + 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 8496fa43ab5c..2360b4d98635 100644 --- a/docs/en/documentation/configuration/skills/_index.md +++ b/docs/en/documentation/configuration/skills/_index.md @@ -40,6 +40,7 @@ toolbox skills-generate \ - `--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 f5c772f74bf6..fad7ebaa63f8 100644 --- a/docs/en/reference/cli.md +++ b/docs/en/reference/cli.md @@ -74,6 +74,7 @@ toolbox skills-generate --name --description --toolset