Skip to content
Closed
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
53 changes: 53 additions & 0 deletions internal/bundle/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,25 @@ func Discover(ctx context.Context, database *store.Store, options DiscoverOption
match := matchedFromObserved(*item, artifact, options.HomeDir)
enrichSkillMatch(&match, *item, skillEvidence)
enrichWorkspaceMatch(&match, *item)
enrichRecipeSource(&match, artifact)
matches[artifact.Key] = append(matches[artifact.Key], match)
matchedBindings[item.binding.ID] = struct{}{}
}
}
for _, probe := range artifact.Probes {
match, ok := resolveProbe(ctx, probe, recipe, artifact, options, lookup)
if ok {
enrichRecipeSource(&match, artifact)
matches[artifact.Key] = append(matches[artifact.Key], match)
}
}
if artifact.Driver == "mainline-hooks" {
hookMatches, hookErr := discoverMainlineHooks(ctx, database)
if hookErr != nil {
return DiscoverResult{}, hookErr
}
matches[artifact.Key] = append(matches[artifact.Key], hookMatches...)
}
matches[artifact.Key] = dedupeMatches(matches[artifact.Key])
}
if countMatches(matches) == 0 {
Expand Down Expand Up @@ -588,6 +597,50 @@ func enrichWorkspaceMatch(match *matchedInstallation, observed observedInstallat
}
}

func enrichRecipeSource(match *matchedInstallation, artifact ArtifactRecipe) {
if strings.TrimSpace(artifact.Source) == "" {
return
}
match.sourceIdentity = strings.TrimSpace(artifact.Source)
if artifact.Subdir != "" {
match.sourceIdentity += "#" + filepath.ToSlash(filepath.Clean(artifact.Subdir))
}
match.metadata["recipe_source_url"] = strings.TrimSpace(artifact.Source)
if artifact.Subdir != "" {
match.metadata["recipe_source_subdir"] = filepath.ToSlash(filepath.Clean(artifact.Subdir))
}
}

func discoverMainlineHooks(ctx context.Context, database *store.Store) ([]matchedInstallation, error) {
projects, err := database.ListProjects(ctx)
if err != nil {
return nil, err
}
var result []matchedInstallation
for _, project := range projects {
if !project.Selected {
continue
}
root := filepath.Clean(project.RootPath)
if info, statErr := os.Stat(filepath.Join(root, ".mainline", "config.toml")); statErr != nil || !info.Mode().IsRegular() {
continue
}
digest := sha256.New()
for _, relative := range []string{".claude/settings.json", ".codex/config.toml", ".codex/hooks.json", ".cursor/hooks.json"} {
data, readErr := os.ReadFile(filepath.Join(root, filepath.FromSlash(relative)))
if readErr == nil {
_, _ = digest.Write([]byte(relative + "\x00"))
_, _ = digest.Write(data)
}
}
result = append(result, matchedInstallation{
path: root, packageIdentity: "mainline-hooks", sourceIdentity: "mainline-hooks:" + root,
hash: hex.EncodeToString(digest.Sum(nil)), metadata: map[string]any{"project_id": project.ID, "derived": true},
})
}
return result, nil
}

func inspectSignedSkillManifest(skillPath string) (string, bool, bool) {
manifestPath := filepath.Join(skillPath, "skill.manifest")
data, err := os.ReadFile(manifestPath)
Expand Down
33 changes: 33 additions & 0 deletions internal/bundle/discover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,39 @@ func TestDiscoverDeduplicatesPhysicalInstallAndReadsPackageMetadata(t *testing.T
}
}

func TestDiscoverMainlineHooksUsesSelectedRepositories(t *testing.T) {
database, err := store.OpenRW(filepath.Join(t.TempDir(), "state.db"))
if err != nil {
t.Fatal(err)
}
defer database.Close()
ctx := context.Background()
selected := t.TempDir()
ignored := t.TempDir()
for _, root := range []string{selected, ignored} {
if err := os.MkdirAll(filepath.Join(root, ".mainline"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, ".mainline", "config.toml"), []byte("[hooks]\nenabled=true\n"), 0o644); err != nil {
t.Fatal(err)
}
}
now := time.Now().UTC()
if err := database.UpsertProject(ctx, model.Project{ID: "selected", RootPath: selected, RootFingerprint: "selected", Selected: true, DiscoveredVia: "test", LastSeenAt: now}); err != nil {
t.Fatal(err)
}
if err := database.UpsertProject(ctx, model.Project{ID: "ignored", RootPath: ignored, RootFingerprint: "ignored", Selected: false, DiscoveredVia: "test", LastSeenAt: now}); err != nil {
t.Fatal(err)
}
matches, err := discoverMainlineHooks(ctx, database)
if err != nil {
t.Fatal(err)
}
if len(matches) != 1 || matches[0].path != selected || matches[0].packageIdentity != "mainline-hooks" {
t.Fatalf("matches = %#v", matches)
}
}

func TestDiscoverPrunesStaleProbeWhenBindingProvidesRicherEvidence(t *testing.T) {
database, err := store.OpenRW(filepath.Join(t.TempDir(), "state.db"))
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions internal/bundle/recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type ArtifactRecipe struct {
Name string `toml:"name" json:"name"`
Kind model.ArtifactKind `toml:"kind" json:"kind"`
Driver string `toml:"driver" json:"driver"`
Source string `toml:"source" json:"source,omitempty"`
Subdir string `toml:"subdir" json:"subdir,omitempty"`
Required bool `toml:"required" json:"required"`
Selectors []Selector `toml:"selectors" json:"selectors"`
Probes []string `toml:"probes" json:"probes,omitempty"`
Expand Down Expand Up @@ -184,6 +186,12 @@ func (r Recipe) Validate() error {
if err := artifact.Kind.Validate(); err != nil {
return err
}
if strings.ContainsAny(artifact.Source+artifact.Subdir, "\x00\r\n") {
return fmt.Errorf("artifact %s source contains invalid characters", artifact.Key)
}
if artifact.Subdir != "" && (filepath.IsAbs(artifact.Subdir) || strings.HasPrefix(filepath.Clean(artifact.Subdir), "..")) {
return fmt.Errorf("artifact %s source subdirectory must be relative", artifact.Key)
}
for _, selector := range artifact.Selectors {
if err := selector.Validate(); err != nil {
return fmt.Errorf("artifact %s: %w", artifact.Key, err)
Expand Down
15 changes: 13 additions & 2 deletions internal/bundle/recipes/agent-capsule.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "agent-capsule"
version = "1"
version = "2"
name = "Agent Capsule"
owner = "delegated"
confidence = "high"
Expand All @@ -13,7 +13,11 @@ kind = "cli"
driver = "github-release"
required = true
probes = ["command:capsule"]
health_argv = ["capsule", "version"]
resolve_argv = ["tooltend", "__bundle-driver", "github-resolve", "z2z23n0/agent-capsule"]
stage_argv = ["tooltend", "__bundle-driver", "github-stage", "z2z23n0/agent-capsule", "capsule", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "github-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "github-rollback", "z2z23n0/agent-capsule", "capsule", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "binary-health", "${path}", "help"]
[[artifacts.selectors]]
field = "name"
equals = "capsule"
Expand All @@ -23,7 +27,14 @@ key = "skill"
name = "Agent Capsule Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/z2z23n0/agent-capsule.git"
subdir = "skills/agent-capsule"
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-release-resolve", "https://github.com/z2z23n0/agent-capsule.git", "z2z23n0/agent-capsule"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/z2z23n0/agent-capsule.git", "skills/agent-capsule", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/z2z23n0/agent-capsule.git", "skills/agent-capsule", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "agent-capsule"
9 changes: 8 additions & 1 deletion internal/bundle/recipes/anysearch.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "anysearch"
version = "1"
version = "2"
name = "AnySearch"
owner = "delegated"
confidence = "high"
Expand All @@ -9,7 +9,14 @@ key = "skill"
name = "AnySearch Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/catoncat/anysearch-skill.git"
subdir = "anysearch"
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-resolve", "https://github.com/catoncat/anysearch-skill.git"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/catoncat/anysearch-skill.git", "anysearch", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/catoncat/anysearch-skill.git", "anysearch", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "anysearch"
9 changes: 8 additions & 1 deletion internal/bundle/recipes/codex-conductor.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "codex-conductor"
version = "1"
version = "2"
name = "Codex Conductor"
owner = "delegated"
confidence = "high"
Expand All @@ -9,7 +9,14 @@ key = "skill"
name = "Codex Conductor Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/catoncat/codex-conductor.git"
subdir = "."
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-resolve", "https://github.com/catoncat/codex-conductor.git"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/catoncat/codex-conductor.git", ".", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/catoncat/codex-conductor.git", ".", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "codex-conductor"
9 changes: 8 additions & 1 deletion internal/bundle/recipes/known-skills.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "0g-hk"
version = "1"
version = "2"
name = "0g-hk"
owner = "delegated"
confidence = "high"
Expand All @@ -10,7 +10,14 @@ key = "skill"
name = "0g-hk Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/catoncat/0g-hk.git"
subdir = "skill-packages/0g-hk"
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-resolve", "https://github.com/catoncat/0g-hk.git"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/catoncat/0g-hk.git", "skill-packages/0g-hk", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/catoncat/0g-hk.git", "skill-packages/0g-hk", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "0g-hk"
20 changes: 18 additions & 2 deletions internal/bundle/recipes/mainline.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "mainline"
version = "1"
version = "2"
name = "Mainline"
owner = "delegated"
confidence = "high"
Expand All @@ -13,7 +13,11 @@ kind = "cli"
driver = "github-release"
required = true
probes = ["command:mainline"]
health_argv = ["mainline", "version"]
resolve_argv = ["tooltend", "__bundle-driver", "github-resolve", "mainline-org/mainline"]
stage_argv = ["tooltend", "__bundle-driver", "github-stage", "mainline-org/mainline", "mainline", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "github-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "github-rollback", "mainline-org/mainline", "mainline", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "binary-health", "${path}", "version"]
[[artifacts.selectors]]
field = "name"
equals = "mainline"
Expand All @@ -26,7 +30,14 @@ key = "skill"
name = "Mainline Skill"
kind = "skill"
driver = "npx-skills"
source = "https://github.com/mainline-org/mainline.git"
subdir = "skills/mainline"
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-release-resolve", "https://github.com/mainline-org/mainline.git", "mainline-org/mainline"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/mainline-org/mainline.git", "skills/mainline", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/mainline-org/mainline.git", "skills/mainline", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "mainline"
Expand All @@ -40,3 +51,8 @@ name = "Mainline generated hooks"
kind = "hook"
driver = "mainline-hooks"
required = false
resolve_argv = ["tooltend", "__bundle-driver", "github-resolve", "mainline-org/mainline"]
stage_argv = ["tooltend", "__bundle-driver", "mainline-hooks-stage", "${path}", "${stage}"]
activate_argv = ["tooltend", "__bundle-driver", "mainline-hooks-activate", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "mainline-hooks-rollback", "${path}", "${stage}"]
health_argv = ["tooltend", "__bundle-driver", "mainline-hooks-health", "${path}"]
15 changes: 13 additions & 2 deletions internal/bundle/recipes/sherlog.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "sherlog"
version = "1"
version = "2"
name = "Sherlog"
owner = "delegated"
confidence = "high"
Expand All @@ -13,7 +13,11 @@ kind = "cli"
driver = "npm"
required = true
probes = ["command:sherlog", "command:shlog"]
health_argv = ["sherlog", "--version"]
resolve_argv = ["tooltend", "__bundle-driver", "npm-resolve", "@act0r/sherlog"]
stage_argv = ["tooltend", "__bundle-driver", "npm-stage", "@act0r/sherlog", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "npm-activate", "${stage}"]
rollback_argv = ["tooltend", "__bundle-driver", "npm-rollback", "@act0r/sherlog", "${rollback_version}", "${stage}"]
health_argv = ["shlog", "--version"]
[[artifacts.selectors]]
field = "name"
equals = "sherlog|shlog"
Expand All @@ -26,7 +30,14 @@ key = "skill"
name = "Sherlog Skill"
kind = "skill"
driver = "npx-skills"
source = "https://github.com/catoncat/sherlog.git"
subdir = "skill-packages/sherlog"
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-npm-release-resolve", "https://github.com/catoncat/sherlog.git", "@act0r/sherlog"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/catoncat/sherlog.git", "skill-packages/sherlog", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/catoncat/sherlog.git", "skill-packages/sherlog", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "sherlog"
Expand Down
9 changes: 8 additions & 1 deletion internal/bundle/recipes/shuorenhua.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "shuorenhua"
version = "1"
version = "2"
name = "shuorenhua"
owner = "delegated"
confidence = "high"
Expand All @@ -9,7 +9,14 @@ key = "skill"
name = "shuorenhua Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/MrGeDiao/shuorenhua.git"
subdir = "."
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-resolve", "https://github.com/MrGeDiao/shuorenhua.git"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/MrGeDiao/shuorenhua.git", ".", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/MrGeDiao/shuorenhua.git", ".", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "shuorenhua"
22 changes: 21 additions & 1 deletion internal/bundle/recipes/xsearch.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema = "bundle-recipe-v1"
id = "xsearch"
version = "1"
version = "2"
name = "xsearch"
owner = "delegated"
confidence = "high"
Expand All @@ -9,7 +9,27 @@ key = "skill"
name = "xsearch Skill"
kind = "skill"
driver = "git-skill"
source = "https://github.com/catoncat/xsearch.git"
subdir = "."
required = true
resolve_argv = ["tooltend", "__bundle-driver", "git-resolve", "https://github.com/catoncat/xsearch.git"]
stage_argv = ["tooltend", "__bundle-driver", "git-stage", "https://github.com/catoncat/xsearch.git", ".", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "git-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "git-rollback", "https://github.com/catoncat/xsearch.git", ".", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "skill-health", "${path}"]
[[artifacts.selectors]]
field = "name"
equals = "xsearch"

[[artifacts]]
key = "binary"
name = "xsearch embedded binary"
kind = "embedded_binary"
driver = "github-release"
required = true
probes = ["path:~/.agents/skills/xsearch/bin/xsearch"]
resolve_argv = ["tooltend", "__bundle-driver", "github-resolve", "catoncat/xsearch"]
stage_argv = ["tooltend", "__bundle-driver", "github-stage", "catoncat/xsearch", "xsearch", "${version}", "${previous_version}", "${stage}", "${path}"]
activate_argv = ["tooltend", "__bundle-driver", "github-activate", "${stage}", "${path}"]
rollback_argv = ["tooltend", "__bundle-driver", "github-rollback", "catoncat/xsearch", "xsearch", "${rollback_version}", "${stage}", "${path}"]
health_argv = ["tooltend", "__bundle-driver", "binary-health", "${path}", "--version"]
Loading
Loading