Watch
1
0
Fork
You've already forked codemode
0
mirror of https://github.com/ralscha/codemode.git synced 2026-10-09 08:18:20 +02:00
Tools for implementing codemode in Go
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ralph Schaer c1967e13c8 upgrade
2026-10-04 10:05:03 +02:00
.github upgrade 2026-07-27 05:52:26 +02:00
cmd/demo initial commit 2026-04-29 19:13:11 +02:00
.golangci.yml upgrade 2026-05-31 15:51:20 +02:00
execute.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
execute_test.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
generator.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
generator_test.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
go.mod upgrade 2026-10-04 10:05:03 +02:00
go.sum upgrade 2026-10-04 10:05:03 +02:00
LICENSE update 2026-07-23 06:28:20 +02:00
normalize.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
README.md Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
schema_types.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
Taskfile.yml upgrade 2026-09-04 15:58:21 +02:00
tool_definition.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
tool_search.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00
tool_search_test.go Harden execution, search, and schema generation 2026-09-06 07:14:31 +02:00

codemode

Search tool definitions, generate a TypeScript API surface for them, and execute small JavaScript programs against caller-provided tool callbacks.

go get github.com/ralscha/codemode

The module targets the Go version declared in go.mod. The root package depends directly on QuickJS and the MCP SDK.

NewToolIndex(...) builds a searchable index over many tool definitions. The index accepts either []codemode.ToolDefinition or []mcp.Tool and searches by name, description, input schema, and output schema.

definitions := []codemode.ToolDefinition{
    codemode.NewToolDefinition("search-products").
        WithDescription("Search the product catalog").
        WithInputSchema(codemode.NewObjectSchema().
            WithProperty("query", codemode.NewStringSchema().WithDescription("Search query")).
            WithRequired("query")).
        WithOutputSchema(codemode.NewObjectSchema().
            WithProperty("items", codemode.NewArraySchema(codemode.NewObjectSchema().
                WithProperty("id", codemode.NewStringSchema().WithDescription("Product identifier")).
                WithProperty("title", codemode.NewStringSchema().WithDescription("Product title")).
                WithRequired("id", "title")).WithDescription("Matched items")).
            WithRequired("items")).
        Build(),
}

index, err := codemode.NewToolIndex(definitions)
if err != nil {
    return err
}

results := index.Query("matched product items", codemode.WithToolSearchLimit(3))
for _, result := range results {
    fmt.Println(result.Definition.Name, result.Score, result.Matches)
}

Query(...) combines BM25-style lexical ranking with exact, normalized, and fuzzy name boosts. Schema property names and descriptions carry more weight than raw schema JSON, so queries like matched product items can find a tool from its output shape. Tools with no positive relevance score are omitted.

QueryRegex(...) provides deterministic filtering:

results, err := index.QueryRegex(`github.*issues`)

Generate API

GenerateFromDefinitions(...) and GenerateFromMCPTools(...) generate TypeScript API declarations. The output uses sanitized method names, generated input and output types, and JSDoc from tool and schema descriptions. Object inputs with no required properties are optional, while tools without an input schema are generated as zero-argument functions.

api, err := codemode.GenerateFromDefinitions(definitions)
if err != nil {
    return err
}
fmt.Println(api)

The generator produces declarations like:

declare const tools : {
 /**
  * Create a new project
  * @param name - Project name
  * @returns id - Project identifier
  */
 create_project: (input: { name: string; }) => { id: string; name: string; };
}

Supported inputs:

  • GenerateFromDefinitions(...): normalized ToolDefinition values
  • GenerateFromMCPTools(...): MCP SDK mcp.Tool values

The schema converter supports objects, arrays, JSON Schema 2020-12 prefixItems tuples, unions and intersections, enums, constants, local $ref values, nullable values, and boolean schemas.

ToolDefinition supports both input and output schemas. The builder APIs keep nested JSON schema values readable:

definition := codemode.NewToolDefinition("create-project").
    WithDescription("Create a new project").
    WithInputSchema(codemode.NewObjectSchema().
        WithProperty("name", codemode.NewStringSchema().WithDescription("Project name")).
        WithRequired("name")).
    WithOutputSchema(codemode.NewObjectSchema().
        WithProperty("id", codemode.NewStringSchema().WithDescription("Project identifier")).
        WithProperty("name", codemode.NewStringSchema().WithDescription("Project name")).
        WithRequired("id", "name")).
    Build()

GenerateFromMCPTools(...) works directly with tool definitions from an MCP server:

api, err := codemode.GenerateFromMCPTools(mcpTools, codemode.WithNamespace("githubTools"))

By default the output starts with declare const tools : { ... }. WithNamespace(...) changes the object name:

githubAPI, _ := codemode.GenerateFromMCPTools(githubTools, codemode.WithNamespace("githubTools"))
stripeAPI, _ := codemode.GenerateFromMCPTools(stripeTools, codemode.WithNamespace("stripeTools"))

Execute

Execute(...) runs JavaScript returned by an LLM. The code executes inside a QuickJS VM with a memory limit and timeout. The caller provides the callback namespaces, so execution stays independent of HTTP clients, databases, or any other tool runtime.

Execution is synchronous. Tool callbacks are exposed as regular JavaScript functions, so generated code should not use await, Promise.all(...), dynamic import(...), or other async-only patterns.

Execute(...) honors context cancellation in addition to its VM evaluation timeout. A canceled context interrupts running JavaScript and is returned as a wrapped context.Canceled or context.DeadlineExceeded error.

Tool callback inputs are JSON objects. Calling a tool without an argument uses an empty object; passing primitive values such as false, 0, or a string is reported as an argument parsing error instead of being silently coerced.

Before evaluation, code is normalized for common LLM output shapes:

  • markdown code fences are stripped
  • bare expressions and final expressions are returned automatically
  • sync arrow functions and simple async () => { ... } wrappers are unwrapped
type WeatherInput struct {
    City string `json:"city"`
}

type WeatherOutput struct {
    City        string `json:"city"`
    Temperature string `json:"temperature"`
}

result, err := codemode.Execute(ctx, `
const weather = tools.get_weather({ city: "Zurich" });
return { text: "Weather in " + weather.city + ": " + weather.temperature };
`, []codemode.ToolCallbackNamespace{
    {
        Callbacks: []codemode.ToolCallbackDefinition{
            codemode.NewToolCallback("get-weather", func(ctx context.Context, input WeatherInput) (WeatherOutput, error) {
                return WeatherOutput{City: input.City, Temperature: "12 C"}, nil
            }),
        },
    },
})
if err != nil {
    return err
}
fmt.Println(result.Value)

Sandboxed code can write diagnostic output with console.log, console.warn, and console.error. Messages are captured in ExecuteResult.Logs; warnings and errors are prefixed with [warn] and [error]. Logs captured before a JavaScript error are still returned with the result value.

The JavaScript API uses the same sanitized names as the generated declarations. For example, get-weather becomes tools.get_weather(...). WithNamespace(...) changes the object name for both generation and execution.

api, _ := codemode.GenerateFromDefinitions(definitions, codemode.WithNamespace("sdk"))
result, _ := codemode.Execute(ctx, `return sdk.search_docs({ query: "install" });`, []codemode.ToolCallbackNamespace{
    {Callbacks: callbacks},
}, codemode.WithNamespace("sdk"))

Multiple namespace entries expose helper objects from multiple generated namespaces to the same JavaScript program.

result, err := codemode.Execute(ctx, `
const repos = githubTools.list_repos({ owner: "ralscha" }).items;
const customers = stripeTools.list_customers({ limit: 2 }).items;
return { repos: repos.length, customers: customers.length };
`, []codemode.ToolCallbackNamespace{
    {Namespace: "githubTools", Callbacks: githubCallbacks},
    {Namespace: "stripeTools", Callbacks: stripeCallbacks},
})

Execution options:

  • WithEvalTimeout(...): limit JavaScript runtime, default 10s
  • WithMemoryLimit(...): limit QuickJS memory, default 32 MiB

Namespace and method names are converted to valid JavaScript identifiers. Namespace names that would shadow execution-runtime globals receive a trailing underscore; for example, console becomes console_ in both generated and executed APIs.

Development

Run the local checks with:

go vet ./...
go test ./...

The GitHub Actions workflow reads the Go version from go.mod, runs go vet, and then runs the test suite. Dependabot is configured for Go modules and GitHub Actions updates.

License

MIT License. See LICENSE for details.