From cacb8977bf81c25ecb74418ea64c360125058535 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 12:33:57 +0000 Subject: [PATCH] feat(agentic): add language detection tools Co-Authored-By: Virgil --- pkg/agentic/commands.go | 3 + pkg/agentic/commands_test.go | 2 + pkg/agentic/lang.go | 107 +++++++++++++++++++++++++++++++++++ pkg/agentic/lang_test.go | 81 ++++++++++++++++++++++++++ pkg/agentic/prep.go | 1 + pkg/agentic/prep_test.go | 2 + 6 files changed, 196 insertions(+) create mode 100644 pkg/agentic/lang.go create mode 100644 pkg/agentic/lang_test.go diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 612cf12..a328e00 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -25,6 +25,8 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("brain/ingest", core.Command{Description: "Bulk ingest memories into OpenBrain", Action: s.cmdBrainIngest}) c.Command("brain/seed-memory", core.Command{Description: "Import markdown memories into OpenBrain from a project memory directory", Action: s.cmdBrainSeedMemory}) c.Command("brain/list", core.Command{Description: "List memories in OpenBrain", Action: s.cmdBrainList}) + c.Command("lang/detect", core.Command{Description: "Detect the primary language for a repository or workspace", Action: s.cmdLangDetect}) + c.Command("lang/list", core.Command{Description: "List supported language identifiers", Action: s.cmdLangList}) c.Command("plan-cleanup", core.Command{Description: "Permanently delete archived plans past the retention period", Action: s.cmdPlanCleanup}) c.Command("pr-manage", core.Command{Description: "Manage open PRs (merge, close, review)", Action: s.cmdPRManage}) c.Command("status", core.Command{Description: "List agent workspace statuses", Action: s.cmdStatus}) @@ -32,6 +34,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("extract", core.Command{Description: "Extract a workspace template to a directory", Action: s.cmdExtract}) s.registerPlanCommands() s.registerTaskCommands() + s.registerLanguageCommands() } // ctx := s.commandContext() diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 4b8ca9c..c816448 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1022,6 +1022,8 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "status") assert.Contains(t, cmds, "prompt") assert.Contains(t, cmds, "extract") + assert.Contains(t, cmds, "lang/detect") + assert.Contains(t, cmds, "lang/list") assert.Contains(t, cmds, "plan") assert.Contains(t, cmds, "plan/create") assert.Contains(t, cmds, "plan/list") diff --git a/pkg/agentic/lang.go b/pkg/agentic/lang.go new file mode 100644 index 0000000..7952153 --- /dev/null +++ b/pkg/agentic/lang.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + + core "dappco.re/go/core" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +var supportedLanguages = []string{"go", "php", "ts", "rust", "py", "cpp", "docker"} + +// input := agentic.LanguageDetectInput{Path: "/workspace/pkg/agentic"} +type LanguageDetectInput struct { + Path string `json:"path,omitempty"` +} + +// out := agentic.LanguageDetectOutput{Success: true, Path: "/workspace/pkg/agentic", Language: "go"} +type LanguageDetectOutput struct { + Success bool `json:"success"` + Path string `json:"path"` + Language string `json:"language"` +} + +// out := agentic.LanguageListOutput{Success: true, Count: 7, Languages: []string{"go", "php"}} +type LanguageListOutput struct { + Success bool `json:"success"` + Count int `json:"count"` + Languages []string `json:"languages"` +} + +type LanguageListInput struct{} + +func (s *PrepSubsystem) registerLanguageCommands() { + c := s.Core() + c.Command("lang/detect", core.Command{Description: "Detect the primary language for a workspace or repository", Action: s.cmdLangDetect}) + c.Command("lang/list", core.Command{Description: "List supported language identifiers", Action: s.cmdLangList}) +} + +// result := c.Command("lang/detect").Run(ctx, core.NewOptions(core.Option{Key: "path", Value: "."})) +func (s *PrepSubsystem) cmdLangDetect(options core.Options) core.Result { + path := optionStringValue(options, "_arg", "path", "repo") + if path == "" { + core.Print(nil, "usage: core-agent lang detect ") + return core.Result{Value: core.E("agentic.cmdLangDetect", "path is required", nil), OK: false} + } + + _, output, err := s.langDetect(context.Background(), nil, LanguageDetectInput{Path: path}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "path: %s", output.Path) + core.Print(nil, "language: %s", output.Language) + return core.Result{Value: output, OK: true} +} + +// result := c.Command("lang/list").Run(ctx, core.NewOptions()) +func (s *PrepSubsystem) cmdLangList(_ core.Options) core.Result { + _, output, err := s.langList(context.Background(), nil, LanguageListInput{}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "%d language(s)", output.Count) + for _, language := range output.Languages { + core.Print(nil, " %s", language) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) registerLanguageTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{ + Name: "lang_detect", + Description: "Detect the primary language for a workspace or repository path.", + }, s.langDetect) + + mcp.AddTool(server, &mcp.Tool{ + Name: "lang_list", + Description: "List supported language identifiers.", + }, s.langList) +} + +func (s *PrepSubsystem) langDetect(_ context.Context, _ *mcp.CallToolRequest, input LanguageDetectInput) (*mcp.CallToolResult, LanguageDetectOutput, error) { + path := core.Trim(input.Path) + if path == "" { + path = "." + } + language := detectLanguage(path) + return nil, LanguageDetectOutput{ + Success: true, + Path: path, + Language: language, + }, nil +} + +func (s *PrepSubsystem) langList(_ context.Context, _ *mcp.CallToolRequest, _ LanguageListInput) (*mcp.CallToolResult, LanguageListOutput, error) { + languages := append([]string(nil), supportedLanguages...) + return nil, LanguageListOutput{ + Success: true, + Count: len(languages), + Languages: languages, + }, nil +} diff --git a/pkg/agentic/lang_test.go b/pkg/agentic/lang_test.go new file mode 100644 index 0000000..8625e73 --- /dev/null +++ b/pkg/agentic/lang_test.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "testing" + + core "dappco.re/go/core" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLang_CmdLangDetect_Good_Go(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + dir := t.TempDir() + require.True(t, fs.Write(core.JoinPath(dir, "go.mod"), "module example").OK) + + r := s.cmdLangDetect(core.NewOptions(core.Option{Key: "_arg", Value: dir})) + require.True(t, r.OK) + + output, ok := r.Value.(LanguageDetectOutput) + require.True(t, ok) + assert.Equal(t, dir, output.Path) + assert.Equal(t, "go", output.Language) +} + +func TestLang_CmdLangDetect_Bad_MissingPath(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdLangDetect(core.NewOptions()) + assert.False(t, r.OK) +} + +func TestLang_CmdLangDetect_Ugly_PreferenceOrder(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + dir := t.TempDir() + require.True(t, fs.Write(core.JoinPath(dir, "go.mod"), "module example").OK) + require.True(t, fs.Write(core.JoinPath(dir, "package.json"), "{}").OK) + + r := s.cmdLangDetect(core.NewOptions(core.Option{Key: "_arg", Value: dir})) + require.True(t, r.OK) + + output, ok := r.Value.(LanguageDetectOutput) + require.True(t, ok) + assert.Equal(t, "go", output.Language) +} + +func TestLang_LangDetect_Good_PHP(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + dir := t.TempDir() + require.True(t, fs.Write(core.JoinPath(dir, "composer.json"), "{}").OK) + + _, output, err := s.langDetect(context.Background(), (*mcp.CallToolRequest)(nil), LanguageDetectInput{Path: dir}) + require.NoError(t, err) + assert.Equal(t, dir, output.Path) + assert.Equal(t, "php", output.Language) +} + +func TestLang_LangList_Good(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + _, output, err := s.langList(context.Background(), (*mcp.CallToolRequest)(nil), LanguageListInput{}) + require.NoError(t, err) + require.True(t, output.Success) + assert.Equal(t, len(supportedLanguages), output.Count) + assert.Equal(t, supportedLanguages, output.Languages) +} + +func TestLang_LangList_Ugly_CopyIsolation(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + _, first, err := s.langList(context.Background(), (*mcp.CallToolRequest)(nil), LanguageListInput{}) + require.NoError(t, err) + require.NotEmpty(t, first.Languages) + first.Languages[0] = "mutated" + + _, second, err := s.langList(context.Background(), (*mcp.CallToolRequest)(nil), LanguageListInput{}) + require.NoError(t, err) + assert.Equal(t, supportedLanguages[0], second.Languages[0]) +} diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 0e28c36..33a64cf 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -347,6 +347,7 @@ func (s *PrepSubsystem) RegisterTools(server *mcp.Server) { s.registerIssueTools(server) s.registerSprintTools(server) s.registerContentTools(server) + s.registerLanguageTools(server) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_scan", diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index ce8b4d2..73a3421 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -584,6 +584,8 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "brain/ingest") assert.Contains(t, c.Commands(), "brain/seed-memory") assert.Contains(t, c.Commands(), "brain/list") + assert.Contains(t, c.Commands(), "lang/detect") + assert.Contains(t, c.Commands(), "lang/list") assert.Contains(t, c.Commands(), "plan-cleanup") assert.Contains(t, c.Commands(), "task") assert.Contains(t, c.Commands(), "task/create")