Skip to content

Commit 17170fa

Browse files
Support agent skills (#79)
* Add skill discovery system for on-demand agent instructions Implement a skill discovery mechanism that finds SKILL.md files from multiple locations (repo, user, system) and makes them available to the agent via the system prompt. Skills use YAML frontmatter for metadata and support precedence ordering where repo skills override user/system skills. Co-authored-by: construct-agent <noreply@construct.sh> * Add SkillService proto definitions for skill management Define the gRPC service and messages for installing, listing, deleting, and updating skills from remote sources (GitHub, GitLab, direct URLs). Co-Authored-By: construct-agent <noreply@construct.sh> * Add skill installation, deletion, and update logic Implements source parsing for GitHub/GitLab repos and direct URLs, git clone operations with go-git, skill discovery, and lock file management with flock for concurrency safety. Co-Authored-By: construct-agent <noreply@construct.sh> * Add SkillService API handler and server registration Implements SkillServiceHandler with install, list, delete, and update operations. Registers the handler in the API server with skill installer created from OS filesystem. Co-Authored-By: construct-agent <noreply@construct.sh> * Add Skill client accessor to API client Adds Skill() method to retrieve SkillServiceClient and updates MockClient with generated mock for testing. Co-Authored-By: construct-agent <noreply@construct.sh> * Add skill CLI commands for install, list, delete, and update Implements construct skill subcommands: - install: Fetch and install skills from GitHub/GitLab/URLs - list: List all installed skills - delete: Remove an installed skill - update: Update skills to latest versions Co-Authored-By: construct-agent <noreply@construct.sh> * Add tests for ParseSource and lock file operations Tests source parsing for GitHub shorthand, full URLs, GitLab URLs, and direct SKILL.md URLs. Also tests lock file create/update/remove operations. Co-authored-by: construct-agent <noreply@construct.sh>
1 parent 8b7cb91 commit 17170fa

26 files changed

Lines changed: 3842 additions & 11 deletions

api/def/construct/v1/skill.proto

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Skill API provides operations for managing skills within Construct.
2+
// Skills are specialized instructions that extend agent capabilities. They can be
3+
// installed from remote repositories (GitHub, GitLab) or direct URLs.
4+
syntax = "proto3";
5+
6+
package construct.v1;
7+
8+
import "buf/validate/validate.proto";
9+
import "google/protobuf/timestamp.proto";
10+
11+
option go_package = "github.com/furisto/construct/api/go/v1";
12+
13+
// SkillService provides operations for managing skills.
14+
// Skills are specialized instructions that can be installed from remote sources
15+
// or placed manually in the skills directory.
16+
service SkillService {
17+
// InstallSkill installs skills from a remote source.
18+
rpc InstallSkill(InstallSkillRequest) returns (InstallSkillResponse) {}
19+
20+
// ListSkills retrieves all installed skills.
21+
rpc ListSkills(ListSkillsRequest) returns (ListSkillsResponse) {
22+
option idempotency_level = NO_SIDE_EFFECTS;
23+
}
24+
25+
// DeleteSkill removes an installed skill.
26+
rpc DeleteSkill(DeleteSkillRequest) returns (DeleteSkillResponse) {}
27+
28+
// UpdateSkill updates installed skills to their latest versions.
29+
rpc UpdateSkill(UpdateSkillRequest) returns (UpdateSkillResponse) {}
30+
}
31+
32+
// Skill represents an installed skill with its metadata and source information.
33+
message Skill {
34+
// name is the unique identifier for the skill (lowercase alphanumeric with hyphens).
35+
string name = 1 [(buf.validate.field).string.min_len = 1];
36+
37+
// description provides a brief summary of the skill's purpose.
38+
string description = 2;
39+
40+
// installed_at is the timestamp when the skill was first installed.
41+
google.protobuf.Timestamp installed_at = 3;
42+
43+
// updated_at is the timestamp when the skill was last updated.
44+
google.protobuf.Timestamp updated_at = 4;
45+
46+
// source indicates where the skill was installed from.
47+
// If not set, the skill was manually placed (local).
48+
oneof source {
49+
GitSource git = 5;
50+
UrlSource url = 6;
51+
}
52+
}
53+
54+
// GitSource represents a skill installed from a git repository.
55+
message GitSource {
56+
// provider indicates which git hosting service the skill came from.
57+
GitProvider provider = 1 [(buf.validate.field).enum.defined_only = true];
58+
59+
// clone_url is the HTTPS URL used to clone the repository.
60+
string clone_url = 2 [(buf.validate.field).string.min_len = 1];
61+
62+
// path is the path within the repository to the skill directory.
63+
string path = 3;
64+
65+
// ref is the branch or tag that was checked out.
66+
string ref = 4 [(buf.validate.field).string.min_len = 1];
67+
68+
// tree_hash is the git tree hash for detecting updates.
69+
string tree_hash = 5;
70+
}
71+
72+
// UrlSource represents a skill installed from a direct URL.
73+
message UrlSource {
74+
// url is the direct URL to the SKILL.md file.
75+
string url = 1 [(buf.validate.field).string.min_len = 1];
76+
}
77+
78+
// GitProvider identifies the git hosting service.
79+
enum GitProvider {
80+
// GIT_PROVIDER_UNSPECIFIED indicates an unknown provider.
81+
GIT_PROVIDER_UNSPECIFIED = 0;
82+
83+
// GIT_PROVIDER_GITHUB indicates GitHub.
84+
GIT_PROVIDER_GITHUB = 1;
85+
86+
// GIT_PROVIDER_GITLAB indicates GitLab.
87+
GIT_PROVIDER_GITLAB = 2;
88+
}
89+
90+
// ListSkillsRequest specifies parameters for listing installed skills.
91+
message ListSkillsRequest {}
92+
93+
// ListSkillsResponse contains the list of installed skills.
94+
message ListSkillsResponse {
95+
// skills is the list of installed skills.
96+
repeated Skill skills = 1;
97+
}
98+
99+
// InstallSkillRequest specifies the source to install skills from.
100+
message InstallSkillRequest {
101+
// source is the installation source (e.g., "owner/repo", "github.com/owner/repo/tree/branch/path",
102+
// or a direct URL ending in SKILL.md).
103+
string source = 1 [(buf.validate.field).string.min_len = 1];
104+
105+
// force overwrites existing skills if they already exist.
106+
bool force = 2;
107+
108+
// skill_names filters which skills to install from the source.
109+
// If empty, all discovered skills are installed.
110+
repeated string skill_names = 3;
111+
}
112+
113+
// InstallSkillResponse contains the results of the installation.
114+
message InstallSkillResponse {
115+
// installed_skills is the list of skills that were successfully installed.
116+
repeated Skill installed_skills = 1;
117+
}
118+
119+
// DeleteSkillRequest specifies which skill to delete.
120+
message DeleteSkillRequest {
121+
// name is the name of the skill to delete.
122+
string name = 1 [(buf.validate.field).string.min_len = 1];
123+
}
124+
125+
// DeleteSkillResponse confirms the skill deletion (empty response).
126+
message DeleteSkillResponse {}
127+
128+
// UpdateSkillRequest specifies which skills to update.
129+
message UpdateSkillRequest {
130+
// name is the name of a specific skill to update.
131+
// If empty, all skills with remote sources are updated.
132+
optional string name = 1;
133+
}
134+
135+
// UpdateSkillResponse contains the results of the update.
136+
message UpdateSkillResponse {
137+
// updated_skills is the list of skills that were successfully updated.
138+
repeated Skill updated_skills = 1;
139+
}

api/go/client/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Client struct {
2222
task v1connect.TaskServiceClient
2323
message v1connect.MessageServiceClient
2424
auth v1connect.AuthServiceClient
25+
skill v1connect.SkillServiceClient
2526
}
2627

2728
type ClientOptions struct {
@@ -106,6 +107,7 @@ func NewClient(endpointContext EndpointContext, options ...ClientOption) (*Clien
106107
task: v1connect.NewTaskServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
107108
message: v1connect.NewMessageServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
108109
auth: v1connect.NewAuthServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
110+
skill: v1connect.NewSkillServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
109111
}, nil
110112
}
111113

@@ -133,13 +135,18 @@ func (c *Client) Auth() v1connect.AuthServiceClient {
133135
return c.auth
134136
}
135137

138+
func (c *Client) Skill() v1connect.SkillServiceClient {
139+
return c.skill
140+
}
141+
136142
type MockClient struct {
137143
ModelProvider *mocks.MockModelProviderServiceClient
138144
Model *mocks.MockModelServiceClient
139145
Agent *mocks.MockAgentServiceClient
140146
Task *mocks.MockTaskServiceClient
141147
Message *mocks.MockMessageServiceClient
142148
Auth *mocks.MockAuthServiceClient
149+
Skill *mocks.MockSkillServiceClient
143150
}
144151

145152
func NewMockClient(ctrl *gomock.Controller) *MockClient {
@@ -150,6 +157,7 @@ func NewMockClient(ctrl *gomock.Controller) *MockClient {
150157
Task: mocks.NewMockTaskServiceClient(ctrl),
151158
Message: mocks.NewMockMessageServiceClient(ctrl),
152159
Auth: mocks.NewMockAuthServiceClient(ctrl),
160+
Skill: mocks.NewMockSkillServiceClient(ctrl),
153161
}
154162
}
155163

@@ -161,6 +169,7 @@ func (c *MockClient) Client() *Client {
161169
task: c.Task,
162170
message: c.Message,
163171
auth: c.Auth,
172+
skill: c.Skill,
164173
}
165174
}
166175

api/go/client/mocks/skill.connect_mock.go

Lines changed: 187 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)