Start here only if you already have a TypeScript plugin. New plugins start from plugin-development.md. This guide maps the old TypeScript SDK onto pkg/plugin.
| Aspect | TypeScript (v1) | Go (v2) |
|---|---|---|
| Runtime | Node.js / Bun | Native binary |
| Package format | npm package | Go binary |
| SDK import | @tinycode/plugin or tinycode-plugin |
github.com/bobbyjohnstx/tinycode/pkg/plugin |
| Entry point | export default { server: ... } |
plugin.Run(plugin.Plugin{...}) |
| Tool definition | tool({ ... }) helper |
plugin.ToolDef{} struct |
| Parameter schema | Zod (tool.schema.string()) |
JSON Schema (map[string]any) |
| Hook registration | Named fields on Hooks object | Fields on plugin.HookHandlers struct |
| Effect system | Effect.gen() / Promises |
Plain Go functions |
| Communication | In-process function calls | JSON-RPC 2.0 over stdin/stdout |
| Distribution | npm publish |
go build + binary in PATH or config dir |
TypeScript:
import type { PluginModule } from "tinycode-plugin"
export default {
server: async (input, options) => {
return {
tool: { ... },
"session.start": async (input) => { ... },
}
},
} satisfies PluginModuleGo:
package main
import "github.com/bobbyjohnstx/tinycode/pkg/plugin"
func main() {
plugin.Run(plugin.Plugin{
ID: "my-plugin",
Tools: []plugin.ToolDef{ ... },
Hooks: plugin.HookHandlers{
SessionStart: func(ctx context.Context, event plugin.SessionStartEvent) error { ... },
},
})
}TypeScript:
import { tool } from "tinycode-plugin/tool"
tool({
description: "Search a knowledge base",
args: {
query: tool.schema.string().describe("The search query"),
limit: tool.schema.number().optional().describe("Max results"),
},
async execute(args, context) {
return `Found results for: ${args.query}`
},
})Go:
plugin.ToolDef{
Name: "search",
Description: "Search a knowledge base",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "The search query",
},
"limit": map[string]any{
"type": "number",
"description": "Max results",
},
},
"required": []string{"query"},
},
Execute: func(ctx context.Context, args json.RawMessage, tc plugin.ToolContext) (string, error) {
var input struct {
Query string `json:"query"`
Limit int `json:"limit"`
}
if err := json.Unmarshal(args, &input); err != nil {
return "", fmt.Errorf("invalid arguments: %w", err)
}
return fmt.Sprintf("Found results for: %s", input.Query), nil
},
}The TypeScript SDK uses Zod for schema validation. The Go SDK uses raw JSON Schema as map[string]any.
| Zod (TypeScript) | JSON Schema (Go) |
|---|---|
tool.schema.string() |
map[string]any{"type": "string"} |
tool.schema.number() |
map[string]any{"type": "number"} |
tool.schema.number().int() |
map[string]any{"type": "integer"} |
tool.schema.boolean() |
map[string]any{"type": "boolean"} |
tool.schema.enum(["a", "b"]) |
map[string]any{"type": "string", "enum": []string{"a", "b"}} |
tool.schema.array(tool.schema.string()) |
map[string]any{"type": "array", "items": map[string]any{"type": "string"}} |
.optional() |
Omit from "required" array |
.describe("text") |
"description": "text" in the property map |
| TypeScript hook | Go field |
|---|---|
"session.start": async (input, output) => {} |
SessionStart: func(ctx context.Context, event SessionStartEvent) error |
"session.end": async (input, output) => {} |
SessionEnd: func(ctx context.Context, event SessionEndEvent) error |
"permission.ask": async (input, output) => {} |
PermissionAsk: func(ctx context.Context, input PermissionInput) (*PermissionOutput, error) |
"shell.env": async (input, output) => {} |
ShellEnv: func(ctx context.Context, input ShellEnvInput) (*ShellEnvOutput, error) |
"tool.execute.before": async (input, output) => {} |
ToolExecBefore: func(ctx context.Context, input ToolExecBeforeInput) error |
"tool.execute.after": async (input, output) => {} |
ToolExecAfter: func(ctx context.Context, input ToolExecAfterInput) (*ToolExecAfterOutput, error) |
dispose: async () => {} |
Dispose: func(ctx context.Context) error |
Key difference: The TypeScript SDK uses an observer pattern where hooks receive (input, output) and mutate output. The Go SDK uses return values -- hooks that need to send data back return a pointer to an output struct. Return nil to pass through without modification.
TypeScript tools can return a string or a structured { title, output, metadata, attachments } object.
Go tools return (string, error). The string is the tool output sent to the LLM. Errors are surfaced as error results.
TypeScript ToolContext includes sessionID, messageID, agent, directory, worktree, abort, metadata(), ask(), progress(), messages(), and sessionInfo().
Go ToolContext is simpler:
type ToolContext struct {
SessionID string `json:"sessionId"`
Directory string `json:"directory"`
}The reduced surface is intentional -- Go plugins run as separate processes and do not have direct access to the tinycode server's internal state.
mkdir my-plugin && cd my-plugin
go mod init github.com/example/tinycode-plugin-my-plugin
go get github.com/bobbyjohnstx/tinycode/pkg/pluginStart with the main() function calling plugin.Run():
package main
import "github.com/bobbyjohnstx/tinycode/pkg/plugin"
func main() {
plugin.Run(plugin.Plugin{
ID: "my-plugin",
Tools: []plugin.ToolDef{ /* ... */ },
Hooks: plugin.HookHandlers{ /* ... */ },
})
}For each TypeScript tool({...}):
- Create a
plugin.ToolDefstruct - Set
Name-- TypeScript tools get their name from the object key; Go tools declare it explicitly - Copy the
Description - Convert Zod args to JSON Schema
Parameters(see the mapping table above) - Convert the
executefunction:- Replace
async execute(args, context)withfunc(ctx context.Context, args json.RawMessage, tc plugin.ToolContext) (string, error) - Define a struct for your args and unmarshal from
json.RawMessage - Replace
return "string"withreturn "string", nil - Replace
throw new Error(...)withreturn "", fmt.Errorf(...)
- Replace
For each TypeScript hook:
- Find the corresponding Go field in
HookHandlers(see mapping table) - Hooks that mutate
outputin TypeScript become hooks that return a pointer in Go:permission.ask: mutatingoutput.statusbecomes returning*PermissionOutputshell.env: mutatingoutput.envbecomes returning*ShellEnvOutputtool.execute.after: mutatingoutput.outputbecomes returning*ToolExecAfterOutput
- Observer-only hooks (
session.start,session.end,tool.execute.before) returnerror
TypeScript plugins use closures for shared state between tools and hooks. Go plugins use the same pattern:
type state struct {
mu sync.Mutex
data map[string]string
}
func newPlugin() plugin.Plugin {
s := &state{data: make(map[string]string)}
return plugin.Plugin{
ID: "my-plugin",
Tools: buildTools(s),
Hooks: buildHooks(s),
}
}Use a mutex to protect shared state -- hooks and tools may be called concurrently.
# Build to config directory
go build -o ~/.config/tinycode/plugins/my-plugin .
# Or install to PATH with conventional name
go build -o $GOPATH/bin/tinycode-plugin-my-plugin .Replace the npm package name in your tinycode config with the Go plugin name:
{
"plugins": ["my-plugin"]
}import type { PluginModule } from "tinycode-plugin"
import { tool } from "tinycode-plugin/tool"
import { exec } from "child_process"
export default {
server: async (input, options) => {
return {
tool: {
notify: tool({
description: "Send a desktop notification",
args: {
title: tool.schema.string().describe("Notification title"),
message: tool.schema.string().describe("Notification body"),
},
async execute(args) {
if (process.platform === "darwin") {
await execPromise(
`osascript -e 'display notification "${args.message}" with title "${args.title}"'`
)
} else {
await execPromise(`notify-send "${args.title}" "${args.message}"`)
}
return "Notification sent"
},
}),
},
}
},
} satisfies PluginModulepackage main
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"runtime"
"github.com/bobbyjohnstx/tinycode/pkg/plugin"
)
type notifyArgs struct {
Title string `json:"title"`
Message string `json:"message"`
}
func main() {
plugin.Run(plugin.Plugin{
ID: "notify",
Tools: []plugin.ToolDef{{
Name: "notify",
Description: "Send a desktop notification",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"title": map[string]any{"type": "string", "description": "Notification title"},
"message": map[string]any{"type": "string", "description": "Notification body"},
},
"required": []string{"title", "message"},
},
Execute: func(ctx context.Context, raw json.RawMessage, _ plugin.ToolContext) (string, error) {
var args notifyArgs
if err := json.Unmarshal(raw, &args); err != nil {
return "", fmt.Errorf("invalid arguments: %w", err)
}
if args.Title == "" || args.Message == "" {
return "", fmt.Errorf("title and message are required")
}
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
script := fmt.Sprintf(
`display notification "%s" with title "%s"`,
args.Message, args.Title,
)
cmd = exec.CommandContext(ctx, "osascript", "-e", script)
case "linux":
cmd = exec.CommandContext(ctx, "notify-send", args.Title, args.Message)
default:
return "", fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("notification failed: %w", err)
}
return "Notification sent", nil
},
}},
})
}TypeScript plugins using the Effect library (Effect.gen(), Effect.map(), etc.) need to be rewritten as plain Go functions. There is no equivalent runtime. Replace Effect pipelines with sequential Go code and explicit error returns.
TypeScript: throw new Error("message") or Effect failures.
Go: Return error as the second value. Always wrap errors with context:
return "", fmt.Errorf("failed to connect: %w", err)TypeScript tools receive validated, typed args thanks to Zod schemas. Go tools receive json.RawMessage and must unmarshal and validate manually:
var args myArgs
if err := json.Unmarshal(raw, &args); err != nil {
return "", fmt.Errorf("invalid arguments: %w", err)
}TypeScript hooks are async (async (input, output) => {}). Go hook functions are synchronous but receive a context.Context for cancellation. For long-running work, respect ctx.Done().
TypeScript hooks mutate an output object:
"permission.ask": async (input, output) => {
output.status = "allow"
}Go hooks return a pointer (or nil to pass through):
PermissionAsk: func(ctx context.Context, input plugin.PermissionInput) (*plugin.PermissionOutput, error) {
return &plugin.PermissionOutput{Allowed: true}, nil
},TypeScript plugins run in-process with access to the tinycode server's internal APIs (client, shell, project info). Go plugins run as separate processes and communicate only via JSON-RPC. This means:
- No direct access to the server's HTTP client
- No
PluginInputwith$shell,project,serverUrl - All communication happens through the defined tool and hook interfaces
TypeScript tools can return structured results ({ title, output, metadata, attachments }). Go tools return (string, error) -- just the output text. If you need structured metadata, encode it in the string output (e.g., as JSON).
TypeScript plugins are resolved via npm package names. Go plugins are resolved by binary name:
- Config dir:
~/.config/tinycode/plugins/<name> - PATH:
tinycode-plugin-<name>
Make sure your binary name matches the plugin ID you use in config.
TypeScript plugins can export a Zod schema to validate options before loading. Go plugins receive options in the InitializeParams.Options field and must validate them manually in the initialize handler. The SDK's Run() function handles the initialize handshake automatically, so custom option validation would need to happen in hook or tool handlers.
The following TypeScript hooks do not have Go equivalents:
chat.message-- message interceptionchat.params-- LLM parameter modificationchat.headers-- HTTP header injectiontool.definition-- tool definition modificationauth-- custom authentication flowsprovider-- custom LLM provider registrationconfig-- config modification at load timeevent-- server event firehosecommand.execute.before-- slash command interception- All
experimental.*hooks
If your TypeScript plugin relies on these hooks, the functionality must be implemented differently (e.g., as a tool, via config, or as a code change to tinycode itself).
The Go plugin registry was consolidated from 42 plugins (~120 tools) to 30 external plugins (~90 tools) plus 4 core builtins. This section covers what changed and what to update in your config.
| Plugin | Replacement |
|---|---|
cluster-ops |
Absorbed into ocp-context-injection (oc_login tool + ShellEnv hook) |
ocp-oauth |
Absorbed into ocp-context-injection (oc_login tool + ShellEnv hook) |
rhoai-mcp-bridge |
Removed — native MCP support in internal/mcp/ replaces it |
snippets |
Removed — LLMs generate better YAML than static templates |
web-search |
Removed — generic DuckDuckGo scraping with limited value |
command-inject |
Removed — arbitrary script execution was a security risk |
eda-events |
Removed — niche hook-only event bridge |
These are always available without any config. Remove them from your "plugins" array if present.
| Former plugin | Builtin behavior |
|---|---|
context-pruning |
Hook-only (ToolExecAfter) — deduplicates repeated tool outputs |
notify |
1 tool — desktop notifications (macOS/Linux) |
code-review |
1 tool — git diff formatted for AI review |
handoff |
1 tool + 3 hooks — session context save/restore |
| Old plugins | New plugin |
|---|---|
rhoai-mlflow-tools + rhoai-experiment-tracker |
rhoai-mlflow (10 tools + 4 hooks) |
rhoai-eval-trustyai + rhoai-model-serving |
rhoai-serving (14 tools) |
The mg_etcd and mg_haproxy tools were removed from ocp-must-gather to avoid overlap with specialist plugins. Use etcd-diag and ingress-inspect instead — they provide deeper, dedicated analysis of the same must-gather data.
If your "plugins" config array contains any removed or promoted plugin name, it is silently skipped at startup (debug-level log, no crash). No config change is required, but cleaning up stale entries is recommended.
tinycode init— optional Red Hat plugin/role setup: prompts for username, picks plugins by role (models via/connect,OPENROUTER_API_KEY, or Ollama)tinycode plugin list --category <slug>— filter plugins by category (sre,security,ai-ml,platform,developer,essential)
Before:
{
"plugins": [
"ocp-oauth", "ocp-context-injection", "cluster-ops",
"context-pruning", "notify", "code-review", "handoff",
"rhoai-mlflow-tools", "rhoai-experiment-tracker",
"rhoai-eval-trustyai", "rhoai-model-serving",
"rhoai-pipelines", "safety-net"
]
}After:
{
"plugins": [
"ocp-context-injection",
"rhoai-mlflow", "rhoai-serving", "rhoai-pipelines",
"safety-net"
]
}Builtins (context-pruning, notify, code-review, handoff) are automatic. ocp-oauth and cluster-ops are absorbed into ocp-context-injection. RHOAI plugins are merged.