package agent import ( "context" "fmt" "os" "path/filepath" "strings" acp "github.com/coder/acp-go-sdk" ) // client implements acp.Client: the callbacks the agent invokes on us. // Session updates and permission requests are delegated to the Handler; file // system access is served directly; terminals are not supported in this // version, matching the capabilities announced during initialize. type client struct { handler Handler } var _ acp.Client = (*client)(nil) func (c *client) SessionUpdate(ctx context.Context, params acp.SessionNotification) error { c.handler.HandleSessionUpdate(ctx, params) return nil } func (c *client) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) { return c.handler.HandlePermissionRequest(ctx, params) } func (c *client) ReadTextFile(_ context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) { if !filepath.IsAbs(params.Path) { return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path) } raw, err := os.ReadFile(params.Path) if err != nil { return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err) } return acp.ReadTextFileResponse{Content: sliceLines(string(raw), params.Line, params.Limit)}, nil } func (c *client) WriteTextFile(_ context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) { if !filepath.IsAbs(params.Path) { return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path) } if dir := filepath.Dir(params.Path); dir != "" { if err := os.MkdirAll(dir, 0o755); err != nil { return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err) } } if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil { return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err) } return acp.WriteTextFileResponse{}, nil } // sliceLines applies ACP's optional 1-based line offset and line count to a // file's content. func sliceLines(content string, line, limit *int) string { if line == nil && limit == nil { return content } lines := strings.Split(content, "\n") start := 0 if line != nil && *line > 0 { start = min(*line-1, len(lines)) } end := len(lines) if limit != nil && *limit > 0 && start+*limit < end { end = start + *limit } return strings.Join(lines[start:end], "\n") } // Terminal support is announced as absent during initialize; a conforming // agent never calls these. They still answer with a proper JSON-RPC error // instead of panicking on a non-conforming one. func (c *client) CreateTerminal(context.Context, acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) { return acp.CreateTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalCreate) } func (c *client) KillTerminal(context.Context, acp.KillTerminalRequest) (acp.KillTerminalResponse, error) { return acp.KillTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalKill) } func (c *client) TerminalOutput(context.Context, acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) { return acp.TerminalOutputResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalOutput) } func (c *client) ReleaseTerminal(context.Context, acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) { return acp.ReleaseTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalRelease) } func (c *client) WaitForTerminalExit(context.Context, acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) { return acp.WaitForTerminalExitResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalWaitForExit) }