Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/internal/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ToolboxOptions struct {
Configs []string
ConfigFolder string
PrebuiltConfigs []string
VersionNum string
}

// Option defines a function that modifies the ToolboxOptions struct.
Expand Down
6 changes: 5 additions & 1 deletion cmd/internal/skills/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type skillsCmd struct {
outputDir string
licenseHeader string
additionalNotes string
invocationMode string
toolboxVersion string
}

// NewCommand creates a new Command.
Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down
85 changes: 50 additions & 35 deletions cmd/internal/skills/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions cmd/internal/skills/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"];`,
Expand All @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions docs/en/documentation/configuration/skills/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ toolbox <tool-source> 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 `<skill-name>` 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`).
Expand Down
2 changes: 2 additions & 0 deletions docs/en/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ toolbox skills-generate --name <name> --description <description> --toolset <too
- `--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).

For more detailed instructions, see [Generate Agent Skills](../documentation/configuration/skills/_index.md).

Expand Down
Loading