Skip to content

Commit 80ef346

Browse files
authored
feat(cli/skills): add support for generating agent skills from toolset (googleapis#2392)
## Description This PR introduces a new skills-generate command that enables users to generate standardized agent skills from their existing Toolbox tool configurations. This facilitates the integration of Toolbox tools into agentic workflows by automatically creating skill descriptions (SKILL.md) and executable wrappers. - New Subcommand: Implemented skills-generate, which automates the creation of agent skill packages including metadata and executable scripts. - Skill Generation: Added logic to generate SKILL.md files with parameter schemas and Node.js wrappers for cross-platform tool execution. - Toolset Integration: Supports selective generation of skills based on defined toolsets, including support for both local files and prebuilt configurations. - Testing: Added unit tests for the generation logic and integration tests for the CLI command. - Documentation: Created a new "how-to" guide for generating skills and updated the CLI reference documentation.
1 parent 732eaed commit 80ef346

File tree

10 files changed

+1368
-140
lines changed

10 files changed

+1368
-140
lines changed

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
yaml "github.com/goccy/go-yaml"
3636
"github.com/googleapis/genai-toolbox/internal/auth"
3737
"github.com/googleapis/genai-toolbox/internal/cli/invoke"
38+
"github.com/googleapis/genai-toolbox/internal/cli/skills"
3839
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
3940
"github.com/googleapis/genai-toolbox/internal/log"
4041
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
@@ -401,6 +402,8 @@ func NewCommand(opts ...Option) *Command {
401402

402403
// Register subcommands for tool invocation
403404
baseCmd.AddCommand(invoke.NewCommand(cmd))
405+
// Register subcommands for skill generation
406+
baseCmd.AddCommand(skills.NewCommand(cmd))
404407

405408
return cmd
406409
}

cmd/skill_generate_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"context"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
"testing"
23+
"time"
24+
)
25+
26+
func TestGenerateSkill(t *testing.T) {
27+
// Create a temporary directory for tests
28+
tmpDir := t.TempDir()
29+
outputDir := filepath.Join(tmpDir, "skills")
30+
31+
// Create a tools.yaml file with a sqlite tool
32+
toolsFileContent := `
33+
sources:
34+
my-sqlite:
35+
kind: sqlite
36+
database: test.db
37+
tools:
38+
hello-sqlite:
39+
kind: sqlite-sql
40+
source: my-sqlite
41+
description: "hello tool"
42+
statement: "SELECT 'hello' as greeting"
43+
`
44+
45+
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
46+
if err := os.WriteFile(toolsFilePath, []byte(toolsFileContent), 0644); err != nil {
47+
t.Fatalf("failed to write tools file: %v", err)
48+
}
49+
50+
args := []string{
51+
"skills-generate",
52+
"--tools-file", toolsFilePath,
53+
"--output-dir", outputDir,
54+
"--name", "hello-sqlite",
55+
"--description", "hello tool",
56+
}
57+
58+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
59+
defer cancel()
60+
61+
_, got, err := invokeCommandWithContext(ctx, args)
62+
if err != nil {
63+
t.Fatalf("command failed: %v\nOutput: %s", err, got)
64+
}
65+
66+
// Verify generated directory structure
67+
skillPath := filepath.Join(outputDir, "hello-sqlite")
68+
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
69+
t.Fatalf("skill directory not created: %s", skillPath)
70+
}
71+
72+
// Check SKILL.md
73+
skillMarkdown := filepath.Join(skillPath, "SKILL.md")
74+
content, err := os.ReadFile(skillMarkdown)
75+
if err != nil {
76+
t.Fatalf("failed to read SKILL.md: %v", err)
77+
}
78+
79+
expectedFrontmatter := `---
80+
name: hello-sqlite
81+
description: hello tool
82+
---`
83+
if !strings.HasPrefix(string(content), expectedFrontmatter) {
84+
t.Errorf("SKILL.md does not have expected frontmatter format.\nExpected prefix:\n%s\nGot:\n%s", expectedFrontmatter, string(content))
85+
}
86+
87+
if !strings.Contains(string(content), "## Usage") {
88+
t.Errorf("SKILL.md does not contain '## Usage' section")
89+
}
90+
91+
if !strings.Contains(string(content), "## Scripts") {
92+
t.Errorf("SKILL.md does not contain '## Scripts' section")
93+
}
94+
95+
if !strings.Contains(string(content), "### hello-sqlite") {
96+
t.Errorf("SKILL.md does not contain '### hello-sqlite' tool header")
97+
}
98+
99+
// Check script file
100+
scriptFilename := "hello-sqlite.js"
101+
scriptPath := filepath.Join(skillPath, "scripts", scriptFilename)
102+
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
103+
t.Fatalf("script file not created: %s", scriptPath)
104+
}
105+
106+
scriptContent, err := os.ReadFile(scriptPath)
107+
if err != nil {
108+
t.Fatalf("failed to read script file: %v", err)
109+
}
110+
if !strings.Contains(string(scriptContent), "hello-sqlite") {
111+
t.Errorf("script file does not contain expected tool name")
112+
}
113+
114+
// Check assets
115+
assetPath := filepath.Join(skillPath, "assets", "hello-sqlite.yaml")
116+
if _, err := os.Stat(assetPath); os.IsNotExist(err) {
117+
t.Fatalf("asset file not created: %s", assetPath)
118+
}
119+
assetContent, err := os.ReadFile(assetPath)
120+
if err != nil {
121+
t.Fatalf("failed to read asset file: %v", err)
122+
}
123+
if !strings.Contains(string(assetContent), "hello-sqlite") {
124+
t.Errorf("asset file does not contain expected tool name")
125+
}
126+
}
127+
128+
func TestGenerateSkill_NoConfig(t *testing.T) {
129+
tmpDir := t.TempDir()
130+
outputDir := filepath.Join(tmpDir, "skills")
131+
132+
args := []string{
133+
"skills-generate",
134+
"--output-dir", outputDir,
135+
"--name", "test",
136+
"--description", "test",
137+
}
138+
139+
_, _, err := invokeCommandWithContext(context.Background(), args)
140+
if err == nil {
141+
t.Fatal("expected command to fail when no configuration is provided and tools.yaml is missing")
142+
}
143+
144+
// Should not have created the directory if no config was processed
145+
if _, err := os.Stat(outputDir); !os.IsNotExist(err) {
146+
t.Errorf("output directory should not have been created")
147+
}
148+
}
149+
150+
func TestGenerateSkill_MissingArguments(t *testing.T) {
151+
tmpDir := t.TempDir()
152+
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
153+
if err := os.WriteFile(toolsFilePath, []byte("tools: {}"), 0644); err != nil {
154+
t.Fatalf("failed to write tools file: %v", err)
155+
}
156+
157+
tests := []struct {
158+
name string
159+
args []string
160+
}{
161+
{
162+
name: "missing name",
163+
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--description", "test"},
164+
},
165+
{
166+
name: "missing description",
167+
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--name", "test"},
168+
},
169+
}
170+
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
_, got, err := invokeCommandWithContext(context.Background(), tt.args)
174+
if err == nil {
175+
t.Fatalf("expected command to fail due to missing arguments, but it succeeded\nOutput: %s", got)
176+
}
177+
})
178+
}
179+
}

docs/en/how-to/generate_skill.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
title: "Generate Agent Skills"
3+
type: docs
4+
weight: 10
5+
description: >
6+
How to generate agent skills from a toolset.
7+
---
8+
9+
The `skills-generate` command allows you to convert a **toolset** into an **Agent Skill**. A toolset is a collection of tools, and the generated skill will contain metadata and execution scripts for all tools within that toolset, complying with the [Agent Skill specification](https://agentskills.io/specification).
10+
11+
## Before you begin
12+
13+
1. Make sure you have the `toolbox` executable in your PATH.
14+
2. Make sure you have [Node.js](https://nodejs.org/) installed on your system.
15+
16+
## Generating a Skill from a Toolset
17+
18+
A skill package consists of a `SKILL.md` file (with required YAML frontmatter) and a set of Node.js scripts. Each tool defined in your toolset maps to a corresponding script in the generated Node.js scripts (`.js`) that work across different platforms (Linux, macOS, Windows).
19+
20+
21+
### Command Usage
22+
23+
The basic syntax for the command is:
24+
25+
```bash
26+
toolbox <tool-source> skills-generate \
27+
--name <skill-name> \
28+
--toolset <toolset-name> \
29+
--description <description> \
30+
--output-dir <output-directory>
31+
```
32+
33+
- `<tool-source>`: Can be `--tools-file`, `--tools-files`, `--tools-folder`, and `--prebuilt`. See the [CLI Reference](../reference/cli.md) for details.
34+
- `--name`: Name of the generated skill.
35+
- `--description`: Description of the generated skill.
36+
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, all tools will be included.
37+
- `--output-dir`: (Optional) Directory to output generated skills (default: "skills").
38+
39+
{{< notice note >}}
40+
**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`).
41+
{{< /notice >}}
42+
43+
### Example: Custom Tools File
44+
45+
1. Create a `tools.yaml` file with a toolset and some tools:
46+
47+
```yaml
48+
tools:
49+
tool_a:
50+
description: "First tool"
51+
run:
52+
command: "echo 'Tool A'"
53+
tool_b:
54+
description: "Second tool"
55+
run:
56+
command: "echo 'Tool B'"
57+
toolsets:
58+
my_toolset:
59+
tools:
60+
- tool_a
61+
- tool_b
62+
```
63+
64+
2. Generate the skill:
65+
66+
```bash
67+
toolbox --tools-file tools.yaml skills-generate \
68+
--name "my-skill" \
69+
--toolset "my_toolset" \
70+
--description "A skill containing multiple tools" \
71+
--output-dir "generated-skills"
72+
```
73+
74+
3. The generated skill directory structure:
75+
76+
```text
77+
generated-skills/
78+
└── my-skill/
79+
├── SKILL.md
80+
├── assets/
81+
│ ├── tool_a.yaml
82+
│ └── tool_b.yaml
83+
└── scripts/
84+
├── tool_a.js
85+
└── tool_b.js
86+
```
87+
88+
In this example, the skill contains two Node.js scripts (`tool_a.js` and `tool_b.js`), each mapping to a tool in the original toolset.
89+
90+
### Example: Prebuilt Configuration
91+
92+
You can also generate skills from prebuilt toolsets:
93+
94+
```bash
95+
toolbox --prebuilt alloydb-postgres-admin skills-generate \
96+
--name "alloydb-postgres-admin" \
97+
--description "skill for performing administrative operations on alloydb"
98+
```
99+
100+
## Installing the Generated Skill in Gemini CLI
101+
102+
Once you have generated a skill, you can install it into the Gemini CLI using the `gemini skills install` command.
103+
104+
### Installation Command
105+
106+
Provide the path to the directory containing the generated skill:
107+
108+
```bash
109+
gemini skills install /path/to/generated-skills/my-skill
110+
```
111+
112+
Alternatively, use ~/.gemini/skills as the `--output-dir` to generate the skill straight to the Gemini CLI.

docs/en/how-to/invoke_tool.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ The `invoke` command allows you to invoke tools defined in your configuration di
2020
1. Make sure you have the `toolbox` binary installed or built.
2121
2. Make sure you have a valid tool configuration file (e.g., `tools.yaml`).
2222

23-
## Basic Usage
23+
### Command Usage
2424

2525
The basic syntax for the command is:
2626

2727
```bash
28-
toolbox [--tools-file <path> | --prebuilt <name>] invoke <tool-name> [params]
28+
toolbox <tool-source> invoke <tool-name> [params]
2929
```
3030

31+
- `<tool-source>`: Can be `--tools-file`, `--tools-files`, `--tools-folder`, and `--prebuilt`. See the [CLI Reference](../reference/cli.md) for details.
3132
- `<tool-name>`: The name of the tool you want to call. This must match the name defined in your `tools.yaml`.
3233
- `[params]`: (Optional) A JSON string representing the arguments for the tool.
3334

docs/en/reference/cli.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ description: >
3232

3333
## Sub Commands
3434

35-
### `invoke`
35+
<details>
36+
<summary><code>invoke</code></summary>
3637

3738
Executes a tool directly with the provided parameters. This is useful for testing tool configurations and parameters without needing a full client setup.
3839

@@ -42,8 +43,36 @@ Executes a tool directly with the provided parameters. This is useful for testin
4243
toolbox invoke <tool-name> [params]
4344
```
4445

45-
- `<tool-name>`: The name of the tool to execute (as defined in your configuration).
46-
- `[params]`: (Optional) A JSON string containing the parameters for the tool.
46+
**Arguments:**
47+
48+
- `tool-name`: The name of the tool to execute (as defined in your configuration).
49+
- `params`: (Optional) A JSON string containing the parameters for the tool.
50+
51+
For more detailed instructions, see [Invoke Tools via CLI](../how-to/invoke_tool.md).
52+
53+
</details>
54+
55+
<details>
56+
<summary><code>skills-generate</code></summary>
57+
58+
Generates a skill package from a specified toolset. Each tool in the toolset will have a corresponding Node.js execution script in the generated skill.
59+
60+
**Syntax:**
61+
62+
```bash
63+
toolbox skills-generate --name <name> --description <description> --toolset <toolset> --output-dir <output>
64+
```
65+
66+
**Flags:**
67+
68+
- `--name`: Name of the generated skill.
69+
- `--description`: Description of the generated skill.
70+
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, all tools will be included.
71+
- `--output-dir`: (Optional) Directory to output generated skills (default: "skills").
72+
73+
For more detailed instructions, see [Generate Agent Skills](../how-to/generate_skill.md).
74+
75+
</details>
4776

4877
## Examples
4978

0 commit comments

Comments
 (0)